JS进阶系列-第六篇-作用域链和闭包

Posted by Kylen on 2019-07-19

作用域

作用域是指程序的源代码中定义变量的区域。

作用域规定了如何查找变量,也就是确定当前执行代码对变量的访问权限。

JavaScript采用词法作用域(lexical scoping),也就是静态作用域

注:好像没听过语法作用域

作用域有静态作用域(词法作用域)和动态作用域之分。它们之间的区别是:静态作用域是在定义的时候确定的,动态作用域是在运行时确定的。前者关心函数在何处声明,后者关心函数在何处调用。

具体的情况也就是:遇到既不是形参也不是函数内部定义的变量的时候,静态作用域去函数定义时的环境中查询,动态作用域去函数调用时的环境中查询。

举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var value = 1;

function foo() {
console.log(value);
}

function bar() {
var value = 2;
foo();
}

bar();

// 结果是 ???

简单分析一下,若是静态作用域,函数foo执行的时候,函数内部查找变量value的值,不是形参,内部也没有,这个时候就从foo函数定义的区域开始找,所以输出1;若是动态作用域,则从函数调用的区域开始找,也就是bar函数内部,所以输出2。

JavaScript采用的是静态作用域,所以这段代码输出1。

JS的作用域链

JS进阶系列-第四篇-执行上下文JS进阶系列-第五篇-变量对象 中我们知道,在查找变量时,会从当前上下文的变量对象中查找,如果找不到,就会从父级(词法层面的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行环境的变量对象构成的链表就是作用域链。

注意:这里的父级执行上下文是依据词法作用域的规则,不是执行阶段的执行栈中的下一级。

举个例子:

1
2
3
4
5
6
7
var scope = "global scope";
debugger
function checkscope(){
var scope2 = 'local scope';
return scope2;
}
checkscope();

执行栈的状态变化
js-advanced 2019-07-18 下午5.31.20.png

checkscope执行上下文的作用域链

  1. 在全局执行上下文中,checkscope函数被创建(注意此时还未进入到checkscope的执行上下文中),全局上下文的变量对象被保存到checkscope的[[scope]]中。
    1
    2
    3
    checkscope.[[scope]] = [
    globalContext.VO
    ]

js-advanced 2019-07-19 上午10.27.58.png
这一步就是关键,父级(词法上的,这里是全局)执行上下文创建的时候,里面的函数checkscope就会保存当前上下文的变量对象(全局变量对象)。即使父级的执行上下文从执行栈中抛出,函数checkscope的上下文依然可以获取到父级的变量对象,这也就是闭包,当然这个例子没有形成闭包。不过从这一步看,正是因为词法作用域的规则,才导致了有闭包的概念,闭包可以说是为了实现词法作用域的一种设计。

2.checkscope函数执行,创建checkscope执行上下文并进入执行栈。checkscope上下文有创建和执行阶段,在创建阶段会创建变量对象、生成作用域链、确定this的指向。作用域链的初始状态就是父级环境的生成时产生checkscope.[[scope]]

1
2
3
checkscopeContext = {
Scope: checkscope.[[scope]]
}

3.checkscope变量对象生成,此时checkscope上下文处于执行栈顶部,变成活动对象

1
2
3
4
5
6
7
checkscopeContext = {
AO: {
arguments: {...},
scope2: undefined // scope2是checkscope上下文的变量,不要搞混了
},
Scope: checkscope.[[scope]],
}

4.活动对象压入作用域链的顶端

1
2
3
4
5
6
7
checkscopeContext = {
AO: {
arguments: {...},
scope2: undefined // scope2是checkscope上下文的变量,不要搞混了
},
Scope: [AO, [[scope]]],
}

5.checkscope上下文执行阶段,赋值语句、函数调用以及其他语句

1
2
3
4
5
6
7
checkscopeContext = {
AO: {
arguments: {...},
scope2: 'local scope'
},
Scope: [AO, [[scope]]],
}

闭包

闭包是指那些能够访问自由变量的函数。自由变量指的是在函数中使用,但既不是函数的参数,也不是函数的局部变量的变量。

也就是说,如果一个函数使用了自由变量,就构成了一个闭包。

也就是说:

1
2
3
4
5
var a = 1;
function foo(){
console.log(a)
}
foo()

foo函数访问了变量a,但a既不是foo的参数,也不是foo内部定义的变量,所以这不是构成了一个闭包吗?
没错,这确实构成了闭包。所以在《JavaScript权威指南》中就讲到:从技术角度讲,所有的JavaScript函数都是闭包。

从理论上讲,所有的函数在创建的时候就已经将上层环境的数据(变量对象)保存起来了([[scope]]属性),这不就是为了实现词法作用域的规则嘛。

这么一看闭包好像没什么特殊的。但闭包却有一个常见的应用场景,这个才是我们口中经常讨论的实践层面的闭包。

** 函数的上层执行上下文已经销毁,已经从执行栈中抛出去了,但是函数依然可以访问上层环境的数据(变量对象)**

举个例子:

1
2
3
4
5
6
7
8
9
10
11
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f;
}

var foo = checkscope();
foo();

执行栈的状态:
js-advanced 2019-07-19 上午11.21.35.png

可以看到,在foo方法执行访问变量scope的时候,执行栈中没有checkscope的执行上下文,但是依然可以访问checkscope中的变量。因为checkscope上下文曾经进入过执行栈,那个时候f函数被创建,同时f函数就已经保留了checkscope执行上下文的数据(变量对象)。f函数的作用域链

1
2
3
fContext = {
Scope: [AO, checkscopeContext.AO, globalContext.VO],
}

参考链接

JavaScript深入之词法作用域和动态作用域
JavaScript深入之作用域链
JavaScript深入之闭包
四、作用域链与闭包