前端JS基础第二篇:作用域与作用域链

目录

作用域与作用域链

什么是作用域

作用域类型

var与let的经典案例

let实现原理

作用域链


作用域与作用域链

什么是作用域

作用域是在程序运行时代码中的某些特定部分中变量、函数和对象的可访问性。
从使用方面来解释,作用域就是变量的使用范围,也就是在代码的哪些部分可以访问这个变量,哪些部分无法访问到这个变量,换句话说就是这个变量在程序的哪些区域可见。代码演示:

function Fun() {

var inVariable = "内部变量";

}

Fun();

console.log(inVariable); // Uncaught ReferenceError: inVariable is not defined

//inVariable是在Fun函数内部被定义的,属于局部变量,在外部无法访问,于是会报错

作用域类型

全局作用域函数作用域、ES6中新增了块级作用域
函数作用域是指声明在函数内部的变量,函数的作用域在函数定义的时候就决定了
块作用域由{ }包括,if和for语句里面的{ }也属于块作用域
在块级作用域中,可通过let和const声明变量,该变量在指定块的作用域外无法被访问

var与let的经典案例

  1. 用var定义i变量,循环后打印i的值
var a = [];

for (var i = 0; i < 10; i++) {

    a[i] = function () {

        console.log(i); // i 指向全局的 i,也就是数组中函数所有的i都指向的是同一个变量i

    };

}

a[6](); // 10

上面代码中,变量i是var命令声明的,在全局范围内都有效,所以全局只有一个变量i。每一次循环,变量i的值都会自增,而循环内被赋给数组a的函数内部的console.log(i),里面的i指向的就是全局的i。也就是说,所有数组a的成员里面的i,指向的都是同一个i,导致运行时输出的是循环结束之后i的值,也就是 10。

扫描二维码关注公众号,回复: 14955729 查看本文章

如果使用let,声明的变量仅在块级作用域内有效,最后输出的是 6。

var a = [];

for (let i = 0; i < 10; i++) {

    a[i] = function () {

        console.log(i); //由于 let 创建的变量是块级作用域的,所以在这个函数内每次都保存一个新的 i

    };

}

a[6](); // 6

这是因为let创建的变量是块级作用域的,所以每次循环都是一个新的 i,每次的值都是 i++ 的结果。因为每次循环的 i 都是一个独立的变量(内存里的唯一地址),因此闭包记录的值都是唯一的,所以才能得到最终的结果

如果用 var 的话,变量 i 是一个全局变量,虽然循环体内每次都创建了一个函数来打印 i,但是当时当刻仅仅是一个指向全局变量 i 的指针,当循环结束之后无论你用哪一个下标去访问循环创建的闭包函数,打印的变量 i 都是全局的那一个,所以全部都是 10。

有的同学可能会感到疑惑,就算i指向全局的那个,为什么全部都是10呢?下面的循环不是正常输出0~9吗:

for (let i = 0; i < 10; i++) {
    console.log(i); //0 1 2 3 4 5 6 7 8 9
}

实际上我们的示例代码相当于:

{
	var i=0;
	a[0] = function () {
		console.log(i)
	}
}
{
	var i=1;
	a[1] = function () {
		console.log(i)
	}
}
{
	var i=2;
	a[2] = function () {
		console.log(i)
	}
}

再看下面这个例子:

<ul>
    <li>1</li>
    <li>2</li>
    <li>3</li>
    <li>4</li>
    <li>5</li>
</ul>
<script>
    // 获取元素
    var lis = document.querySelectorAll('li');
    for(var i = 0; i<lis.length; i ++){
        lis[i].onclick = function(){
        	console.log(i);//5  不能使用i
        }
    }
</script>

当点击事件触发时,for循环已经执行结束了,由于i是var定义的,后面定义的i会覆盖前面的,所以最后i为5,当执行函数时,函数内没有i,根据作用域链向父级找,得到的是全局变量i = 5。聪明的小伙伴肯定可以举一反三,如果函数内不是点击事件是类似setTimeout这类的异步操作,最后的结果也是一个道理。

let实现原理

利用 JS 中基本类型(Primitive Type)的参数传递是按值传递(Pass by Value)的特征,不难改造出下面的代码:

// 用var实现案例2的效果
var a = [];

var _loop = function _loop(i) {
  a[i] = function() {
    console.log(i);
  };
};

for (var i = 0; i < 10; i++) {
  _loop(i);
}
a[0](); // 0

作用域链

当查找变量的时候,首先会先从当前上下文的变量对象(作用域)中查找,如果没有找到,就会从父级的执行上下文的变量对象中查找,如果还没有找到,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链

看下面这段代码:

var a = 'jack';
var b = 'andy';
function fn() {
    var a = 'frank';
    console.log(a);
    console.log(b);
}
fn();
console.log(a);

它的输出结果是 frank andy jack

第二个console.log为什么会输出全局变量andy呢?

这个时候就有了作用域链的概念了,简单的说作用域表示区域,作用域链表示次序
现在我们把眼光放在函数fn里,第一行定义了a是局部变量,第二行输出这个a,但是整个代码里定义了两个a,那么就需要刚刚说到的作用域链来决定到底先用哪个变量。
js会先看函数内有没有这个变量a,如果没有再去函数的外围看有没有这个变量,这里作用域链就帮我们安排好了这个次序。
所以,函数内定义了变量a为frank,那么第二行就会输出frank,第三行要输出变量b,我们先看函数内有没有这个变量,发现没有,再去外围发现有全局变量b,那么输出的就是这个值,我们再来看最后一console.log(a),因为他在全局范围内,所以只能访问全局变量a。

也就是说:作用域链只能向上查找,最终找到全局。不能同级(局部)或者向下查找

我们再看这一段代码:

var a = 'jack';
function fn() {
    console.log(a);
    var a = 'andy';
    console.log(a);
}
fn();

我们思考一下会输出什么呢?结果是 undefined andy

稍微有点js经验的同学应该都会答对,因为有变量提升,变量a在第一行就被声明了,只不过没有被赋值。下面修改一下代码,下面再看看会输出什么:

var a = 'jack';
function fn() {
    console.log(a);
    var a = 'andy';
    console.log(ss());
    function ss() {
        return a;
    }
}
fn();

我们在fn函数内又添加了一个函数ss,并且在这个函数的顶部就调用了这个函数。不仅函数内声明的变量会被提升,函数内的函数也会被提升,而且函数的提升会比变量更优先

那么,在javascript中这段代码实际上是这样被执行的:

var a = 'jack';
function fn() {
    function ss() {
        return a;
    }
    var a;
    console.log(a);
    a = 'andy';
    console.log(ss());
}
fn();

先把函数声明提升到首行,再声明变量a,然后输出aa没有被赋值,所以是undefined,然后a被赋值为'andy',最后调用函数ss,返回的就是andy

猜你喜欢

转载自blog.csdn.net/qq_49900295/article/details/127936828