javascript笔记之作用域链及闭包

在执行上下文的创建阶段,会分别生成变量对象,建立作用域链,确定this指向。其中变量对象this指向已经总结过了,今天就是要聊聊作用域的建立,以及闭包。

很多人将作用域链和调用栈混为一谈,一定要记住作用域链顺序和调用栈(执行上下文组成的链)的顺序没有关系。原因:JavaScript的作用域(scope)是静态作用域,作用域是在函数定义的时候就被确定了,所以作用域链也是在函数定义的时候就被确定了,是根据函数定义的位置来计算的。而调用栈是在函数执行时动态生成的,跟函数被调用相关,跟函数定义无关。

作用域链的作用

作用域链(scope chain)的用途是保证对执行上下文有权访问的所有变量和函数的有序访问,作用域链的最前端始终都是当前执行的代码所在环境的变量对象。。

很拗口,画重点:作用域链是由函数代码所处位置的上层以及更上层函数的变量对象组成。

放大镜里的作用域链

前面说过作用域链是由函数定义的时候确定的,那么到底是怎么确定的,通过模拟静态作用域的语法分析树,来看下作用域链是怎么在定义的时候被确定的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/*全局(window)域下的一段代码*/
var a = 1,b = 2;
function test(c, d){
var e = 4;
console.log(a);
function foo(f,g) {
a = 11;
var h = 5;
function jec(){}
function bar(k){
console.log(k);
};
bar(60);
};
foo(40,50);
}
test(10,20);

上面的代码很简单,定义了一些全局变量和全局方法,然后在方法内又定义局部变量和局部方法,运行时,JS 引擎会先通过语法分析和预解析得到语法分析树,我们重点就来看看这个树都有些什么信息。

为了清晰表示个各种对象间的引用关系,我们用一个简单的伪对象表示上面的代码的语法分析树。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
/**
* 模拟建立一棵语法分析树,存储function内的变量和方法
*/
var SyntaxTree = {
// 全局对象在语法分析树中的表示
window: {
variables: {
a: { value: 1 },
b: { value: 2 }
},
functions: {
test: this.test
}
},
test: {
variables: {
e: undefined
},
functions: {
foo: this.foo
},
scope: this.window
},
foo: {
variables: {
h: undefined
},
functions: {
jec: this.jec,
bar: this.bar
},
scope: this.test
},
jec: {
variables: {},
functions: {},
scope: this.foo
},
bar: {
variables: {
k: undefined
},
functions: {},
scope: {
myname: bar,
scope: this.foo
}
}
};

根据语法分析树中当前函数对应的scope属性,就可以勾勒出一个链,这个就是作用域链,变量查找就是跟着这条链条的变量对象查找的

PS:很多时候作用域链都被认为是包含的结构,其实并不是,它是一个单向通道的链条,以上例中的函数bar为例,我们可以通过一个数组来表示,它作用域链上的变量对象应该是这样的[VO(bar), VO(foo), VO(test), VO(window)], 查找变量的先后顺序应该是这样的VO(bar) => VO(foo) => VO(test) => VO(window)

闭包

闭包又称为词法闭包, 如果函数B访问了函数A执行上下文的变量对象,那么函数A就是一个闭包。而函数B,即使在创建它的函数A的上下文被销毁的时候,它依然存在。

看下一个闭包的代码:

1
2
3
4
5
6
7
8
9
10
11
function A(){
var a = 10;

var b = function(){
console.log(a);
}
return b;
}

var b = A();
b();

通过chrome的call stack可以看到形成一个闭包

闭包的变量查找规则和作用域链规则跟正常的函数是一样的,唯一不同的是变量a的释放时机,取决于b没有了引用。

面试必刷题

利用闭包,修改下面的代码,让循环输出的结果依次为1, 2, 3, 4, 5

1
2
3
4
5
for (var i=1; i<=5; i++) {
setTimeout( function timer() {
console.log(i);
}, i*1000 );
}

总结

从语法分析树角度来分析作用域链,虽然自己感觉已经讲的很清楚了,但是理解作用域链不是一件简单的事情,如果有什么问题,欢迎给我留言。

理解了作用域链和变量对象,闭包看起来就很简单了,只是函数B运行时使用了函数A的变量对象,这时候A就被成为闭包。