JavaScript 作用域与作用域链( for 循环中 let 与 var 的区别 )

174 阅读4分钟

作用域(Scope)

在 JavaScript 中,什么是作用域( Scope )?

我们不妨看看以下代码:

(function(){
    var text = "Hello,world!";
})();
console.log(text);

当我们试图访问立即执行函数((function(){....})())中声明的 text 变量时,造成了ReferenceError: text is not defined 错误。

可见,一个函数内部的变量是不能被函数外部访问的,而变量的可访问性就是作用域了。

目前 JavaScript 中一共有三种作用域,分别是:

  1. 全局作用域
  2. 函数作用域
  3. 块级作用域

顾名思义,全局作用域作用于全部代码,处于全局作用域的对象可以在任何地方被访问。

以下几种情形的变量拥有全局作用域:

一种情况是:定义在最外层函数的变量

var text = "Hello,world!"
function print(){
    console.log(text);
}
print();
console.log(text);

输出:

Hello,world!
Hello,world!

同时,text 变量也自动成为了 window 对象中的成员。

<script>
    var text = "Hello,world!"
    console.log(window.text);
</script>

输出:

Hello,world!

并且,所有 window 对象的属性都拥有全局作用域。

还有一种情况,未被定义但被赋值的变量自动拥有全局作用域:

<script>
    (function(){
        text = "Hello,world!";//可以对照下开头的例子:var text = "Hello,world!";
    })();
    console.log(text);
    console.log(window.text);
</script>

输出:

Hello,world!
Hello,world!

函数作用域是指定义在某个函数中所拥有的作用域。文章开头的示例,正是函数外部访问函数内部变量造成的错误。

(function(){
    var text = "Hello,world!";
    (function(){
        console.log(text);
        text = "Hi!";
        console.log(text);
    })();
})();

输出:

Hello,world!
Hi!

尝试外部访问:

(function(){
    var text = "Hello,world!";
})();
console.log(text);//ReferenceError: text is not defined

作用域好比套娃,层层嵌套。同时,内部作用域可以访问外部作用域的对象,反之则不行。

值得注意的是,JavaScript 不同其他语言,用 var 定义的变量并不具有块级作用域。

// if 中的 text 变量 可以被外部访问
if(true)
{
    var text = "Hello,world!";
}
console.log(text);

输出:

Hello,world!

显然,如果没有块级作用域的话很容易造成变量的命名冲突。好在 E6 之后,JavaScript 提供了 let、const 关键字来让变量具有块级作用域。

// 将 var 替换为 let
if(true)
{
    let text = "Hello,world!";
}
console.log(text);

ReferenceError: text is not defined

  1. 不同于 var 声明的变量,let/const 声明的变量不会有变量提升:
if(true)
{
    console.log(text);
    let text = "Hello,world!";
}

ReferenceError: Cannot access 'text' before initialization

如果你需要让 let/const 声明的变量能在整个块级作用域被访问,最简单的方法就是将声明变量的代码提至块级作用域的顶端。这里不再演示了。

  1. 不允许重复声明:
let text = "Hello,world!";
var text = "Hello,world!";//let text = "Hello,world!"; 同理

SyntaxError: Identifier 'text' has already been declared

但是它允许在不同作用域重复声明:

var text = "Hello,world!";//let text = "Hello,world!"; 同理
if(true){
    let text = "Hello,world!";
}
console.log(text);

输出:

Hello,world!

猜一猜以下代码的输出:

var events = new Array();
for(var i = 0;i < 3;i++){
    events[i] = function(){console.log(i)};
}
events.forEach(e => {
    e();
});

0,1,2 或 3,3,3 ?

仔细分析,events 每个方法输出的是变量 i,方法里面没有变量 i,遵循作用域的原则,向上寻找变量 i,找到了循环变量 i ,而 i 最后值为 3,所以输出结果应该为:3,3,3。

这似乎不是我们想要的,如何变成 0,1,2 呢?我们可以将它包裹在立即执行函数内:

var events = new Array();
for(var i = 0;i < 3;i++){
    (function(n){
        events[i] = function(){console.log(n)};
    })(i);
}
events.forEach(e => {
    e();
});

因为每个变量 i 都是独立的,所以最终的输出为:0,1,2。

更为快捷的方法是直接将 var 替换为 let

为何这是可行的呢?哪怕 let i 处于块级作用域,按照作用域原则,访问 i 时,它还是为 3。

JavaScript 引擎内部会记住上一轮循环的值,初始化本轮的变量 i 时,就在上一轮循环的基础上进行计算。

我们可以认为 let 声明的循环变量,JavaScript 每次都会独立产生一个块级作用域,所以,每个 i 是处于不同作用域的,使得输出结果不同。

for 循环部分的模拟代码可以看作:

//……
let i = 0;
if(i < 3){
    let k = i;
    events[i] = function(){console.log(k)};
}
i++;
if(i < 3){
    let k = i;
    events[i] = function(){console.log(k)};
}
i++;
if(i < 3){
    let k = i;
    events[i] = function(){console.log(k)};
}
i++;
//……

作用域链(Scope chain)

前面简单介绍了:内部作用域内可以访问外部作作用域的对象。

这种不在内部作用域但可以访问的变量叫做自由变量

var text = "Hello,world!"
function print(){
    console.log(text);//这里的 text 就是自由变量
}
print();

上述代码中,print 方法中并没有定义 text 变量,于是在访问它时,将向上一层作用域寻找 text,并找到了它。

作用域链表示的便是作用域之间的层级关系。

自由变量的取值值得考量:

var text = "Hello,world!"
function print(){
    console.log(text);
}
function test(){
    var text = "Hi!";
    print();
}
test();

如果自由变量的取值是简单地按照代码向上一级的作用域寻找变量,那么输出结果无疑是:Hi!。但在这里他的输出是:

Hello,world!

所以,自由变量的取值需要找到创建函数的作用域然后再向上寻找,而非调用函数的作用域。

var i = 10;
function sum(){
    var j = -10;
    return (function(){
        i += j;
    });
}
var j = 10;
sum()();
console.log(i);

0 或 20?

注意,它的流程为:调用 sum()() > i += j > 变量 j 为自由变量 > 从创建该函数的上一级( sum )寻找 > var j = -10,所以,结果为 0。