2019-04-08
作用域链与闭包
作用域
- 在JavaScript中,我们可以将作用域定义为一套规则,这套规则用来管理引擎如何在当前作用域以及嵌套的子作用域中根据标识符名称进行变量查找。
这里的标识符,指的是变量名或者函数名
- Javascript中只有全局作用域与函数作用域。(eval我们平时开发中几乎不会用到它)
- 作用域与执行上下文是两个不同的概念。
JavaScript代码的整个执行过程,分为两个阶段,代码编译阶段与代码执行阶段。编译阶段由编译器完成,将代码翻译成可执行代码,这个阶段作用域规则会确定。执行阶段由引擎完成,主要任务是执行可执行代码,执行上下文在这个阶段创建。
image.png
作用域链
image.png我们知道函数在调用激活时,会开始创建对应的执行上下文,在执行上下文生成的过程中,变量对象,作用域链,以及this的值会分别被确定。
作用域链,是当前环境与上层环境的一系列变量对象组成,它保证了当前环境对符合访问权限的变量和函数的有序访问。
举个栗子:)
var a = 20;
function test() {
var b = a + 10;
function innerTest(){
var c =10;
return b+c;
}
return innerTest();
}
test();
在上面的栗子:)中,全局,函数test,函数的innerTest的执行上下文先后创建。我们设定他们的变量对象分别为VO(global),VO(test),VO(innerTest)。而innerTest的作用域链,则同时包含了这三个变量函数,所以innerTest的执行上下文可如下表示。
innerTestEC = {
VO: {. . .}. //变量对象
scopeChain: [VO(innerTest), VO(test),VO(global)], //作用域链
}
我们可以直接用一个数组来表示作用域链,数组的第一项scopeChain[0]为作用域链的最前端,而数组的最后一项,为作用域的最末端,所有的最末端都为全局变量对象。
很多人会误解为当前作用域与上层作用域为包含关系,但其实不是。以最前端为起点,最末端为终点的单方向通道我认为是更加贴切的形容。如图:
image.png
注意,因为变量对象在执行上下文进入执行阶段时,就变成了活动对象,这一点在上一篇文章中已经讲过,因此图中使用了AO来表示。Active Object
作用域链是由一系列变量对象组成,我们可以在这个单向通道中,查询变量对象中的标识符,这样就可以访问到上一层作用域中的变量了。
闭包
闭包是一种特殊的对象。
它由两部分组成。执行上下文(代号A),以及在该执行上下文中创建的函数(代号B)。
当B执行时,如果访问了A中变量对象中的值,那么闭包就会产生。
在大多数理解中,包括许多著名的书籍,文章里都以函数B的名字代指这里生成的闭包。而在chrome中,则以执行上下文A的函数名代指闭包。
举一个简单的栗子:)
var fn = null;
function foo() {
var a = 2;
function innnerFoo() {
console.log(a);
}
fn = innnerFoo; // 将 innnerFoo的引用,赋值给全局变量中的fn
}
function bar() {
fn(); // 此处的保留的innerFoo的引用
}
foo();
bar(); // 2
在上面的例子中,foo()执行完毕之后,按照常理,其执行环境生命周期会结束,所占内存被垃圾收集器释放。但是通过fn = innerFoo,函数innerFoo的引用被保留了下来,复制给了全局变量fn。这个行为,导致了foo的变量对象,也被保留了下来。于是,函数fn在函数bar内部执行时,依然可以访问这个被保留下来的变量对象。所以此刻仍然能够访问到变量a的值。
这样,我们就可以称foo为闭包。