作用域链的知识的建立在执行上下文的基础上的。
在介绍执行上下文的时候,说到每一次函数调用都会产生一个执行上下文,这个执行上下文里面有一个东西叫 VO, VO 里面有参数、变量、函数。实际上,除此之外,VO 里面还有一个额外的属性,这个属性指向创建该 VO 的函数本身(GO除外)。
每一个函数在创建的时候也有一个叫做 Scopes 的属性,这个属性指向创建该函数时的 AO,注意,这里容易混淆,函数的创建和函数的执行不同,函数的创建实际上是在预编译阶段就完成了,比如我叫函数 A,我内部有一个函数 B,包含了函数 B 的定义和执行两部分,在我执行之前,我就要把函数 B 放到我的 VO 中去,此时函数 B 就可以看做被创建了。而此时函数 B 被创建之后,JS 引擎还是在执行我函数 A,也就是说此时的 AO 还是我,所以此时 B 中的 scopes 属性还是指向我函数 A。
js
var g = 1;
function A(){
function B(){
console.log(g);
}
B();
}
A();
此时代码开始执行,第一行让 g 变成 1

然后执行函数 A,此时会创建函数 A 的执行上下文

注意这张图,创建 A 函数的 VO 的时候,A 函数 VO 中会有 B 函数,同时,B 函数此时被创建了,此时他的 scopes 属性指向 B 当前栈顶的 VO,实际上应该叫做 AO。除此之外,VO 中也应该有一个隐藏属性,指向该函数本身。
此时继续运行函数 B,函数 B 内没有声明任何变量和函数,所以 VO 中只有一个隐藏属性指向函数 B 本身

接下来,在函数 B 中要访问变量 g,由于 B 的 VO 中没有变量 g,此时会沿着这个隐藏属性找到函数 B 本身,再通过函数 B 的 scopes 属性找到它上一层的 VO,此时 A 的 VO 中也没有,一样,会沿着隐藏属性找到函数 A 本身,再通过函数 A 的 scopes 属性找到全局环境的 VO,也就是 GO,此时 GO 中有 g,就会使用全局环境中的 g。
为什么函数内部可以访问到外部的变量,其实本质上就是通过这种方式实现的,所以实际上,虽然多个执行上下文看似是独立的,实则是通过一系列隐藏属性联系在一起的。
对于字面量声明的函数,在预编译阶段就会直接创建函数,并在其上一层的 VO 中产生一个变量引用。但是如果是函数表达式赋值的方式创建的函数,需要在赋值的时候产生函数,并在其上一层的 VO 中产生一个引用。
这里读者可以看到,一个函数的 scopes 属性是在函数创建时就已经确定的,而不是在函数运行时再确定的,也就是说函数确定 scopes 属性的时候,实际上函数还没有运行呢,所以这个函数不管在什么地方执行,scope 属性都不会有任何改变。这其实有一个名字,叫做静态作用域,或者词法作用域。而相对应的,如果作用域是在函数的执行位置确定的,那这就叫做动态作用域。现在主流编程语言均采用静态作用域。
接下来,我们按照同样的逻辑来分析下面代码的执行过程
js
function A(){
var count = 0;
return function(){
count++;
console.log(count);
}
}
var test = A();
test();
test();
test();首先是全局环境会进行预编译,在全局环境中,有一个变量 test ,预编译时为 undefined,然后有一个函数 A,所以会创建该函数,并设置其 Scopes 属性

此时开始执行代码,执行了 A 函数,在执行 A 函数之前,要对 A 函数进行预编译,或者说是创建 A 函数的执行上下文,A 函数内部只声明了一个变量,注意,返回的函数是作为表达式的形式存在的,所以预编译的时候并不会加到 VO 中去

紧接着执行代码,将 count 赋值为 0

此时返回一个函数,此时要先创建这个函数

此时 A 执行完毕,A 的执行上下文就应该出栈,A 的返回值就是这个函数,并且在外部将 test 变量赋值为了该函数,如下所示

此时我们运行 test 函数,先创建 test 的执行上下文,由于 test 函数就是返回出来的那个函数,我们可以看到这个函数里面没有用 var 声明任何变量,也没有什么参数和函数,所以其 VO 里面实际上只有一个指向函数 test 的隐藏属性

此时执行 count++,由于 test 函数中的 VO 没有 test 属性,所以就会沿着这个隐藏属性,找到 test 函数本身的 scopes,也就是标红的位置

此时这里有 count 变量,就会把它+1,然后输出

所以第一次输出就是 1,紧接着这个 test 函数运行结束,销毁这个 test 函数

这里读者可能会有一些疑问,为什么销毁 test 函数之后 test 的 VO 被跟着销毁了,但是 A 函数的 VO 为什么还在这呢?因为函数 test 的
scopes属性指向了 A 函数的 VO,所以当 A 函数的执行上下文被销毁的时候,A 函数的 VO 被保存下来了。只有当我们没有任何办法去访问的对象,JS 才会将其销毁,而这里 A 函数的 VO 实际上是可以通过 test 函数访问到的。只有当我把 test 赋值为 null,让 test 不指向这个函数,此时这个匿名函数就会被销毁,同时,由于没有任何东西能访问到 A 函数的 VO,此时 A 函数的 VO 才会被销毁掉。
同样,再执行一次 test 函数,这个 test 函数又会按照同样的步骤让 A 函数 VO 中的 count 属性加一并输出,这里执行了三次 test 函数,所以会依次输出 1,2,3

如果读者细心,会发现一个问题,就是这个 A 函数的 VO 之所以没有被销毁,是因为 test 函数的 scopes 属性指向了他,并且有且只有这一个地方指向了他,所以我们如果此时想要读写这个 count 属性,除了 test 函数就毫无办法,此时这个 A 函数的 VO 实际上就可以看做 test 函数的一个私有空间,count 变量似乎是 test 函数的私有变量一样,这就引出了闭包的一个常见应用场景,变量私有化。实际上,这也很类似于其他编程语言中的静态变量。
实际上,闭包就是这么个东西,闭包其实就是函数的执行上下文已经消失了,但是其 VO 还是被某些引用类型保存了下来,让其无法被销毁。从广义上来看,闭包实际上就是函数内部访问到了函数外部的变量。
利用闭包的特点,我们可以实现对象的私有属性
js
function A(){
var a = 1;
return {
getA:function(){
return a;
},
setA:function(n){
a = n;
}
}
}
var obj = A();
console.log(obj.getA());此时这个 a 就可以看做是对象 obj 的私有属性,我们无法在外面任何地方读写这个 a 变量,只能通过这个对象提供的 setA 和 getA 两个方法来操作 a 变量。
在一些浏览器中会对作用域链进行优化,只保留需要用到的数据