JavaScript进阶笔记(二):作用域链和闭包

一、闭包

闭包是指有权访问另一个函数作用域中的变量的函数。 — 红宝书

闭包是一个函数;能够访问另一个函数作用域中的变量。

1.1 闭包的特点

  • 闭包可以访问当前函数以外的变量。
  • 即使外部函数已经返回,闭包仍能访问外部函数定义的变量。(作用域链)
  • 闭包可以更新外部变量的值。

二、作用域链

当访问一个变量时,解释器会在当前作用域中查找标示符,如果没找到就去父作用域找,直到找到或者抛出 ReferenceError,最顶端是全局对象。这就是作用域链

作用域链和继承查找的区别:继承查找在当前对象和原型都找不到,会返回 undefined;但作用域链查找找不到则会抛出 ReferenceError。

三、测试题

1
2
3
4
5
6
7
8
9
10
11
var data = [];

for (var i = 0; i < 3; i++) {
data[i] = function () {
console.log(i);
};
}

data[0](); // 3
data[1](); // 3
data[2](); // 3

循环结束后,全局执行上下文 VO 是:

1
2
3
4
5
6
globalContext = {
VO: {
data: [...],
i: 3
}
}

执行 data[0] 函数时,作用域链为:

1
2
3
data[0]Context = {
Scope: [AO, globalContext.VO]
}

自身没有 i 变量,向上查找,此时全局上下文中 i 为 3。

3.1 解决方法

不去读全局而是维护一份副本。

3.1.1 立即执行

1
2
3
4
5
6
7
8
9
var data = [];

for (var i = 0; i < 3; i++) {
(function (num) {
setTimeout(function () {
console.log(num)
}, 1000);
})(i);
}

3.1.2 匿名函数赋值

1
2
3
4
5
6
7
8
9
var data = [];

for (var i = 0; i < 3; i++) {
data[i] = (function (num) {
return function () {
console.log(num);
}
})(i);
}

使用ES6中的 let

1
2
3
4
5
6
7
8
var data = [];

for (let i = 0; i < 3; i++) {
data[i] = function () {
console.log(i);
};
}
// let 会创建块级作用域。

四、作用域链与闭包

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()
console.log(foo()) // local scope

调用栈执行顺序:

1
2
3
4
5
Stack.push(<global>)
Stack.push(<checkscope>)
Stack.pop()
Stack.push(<f>)
Stack.pop()

函数 f 的执行是在 checkscope函数上下文被销毁之后,那么为何还能读取到 scope 变量?

函数f执行后会维护一个作用域链,会指向checkscope 作用域,作用域链是一个数组。
如下:

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

所以指向关系是当前作用域 --> checkscope作用域--> 全局作用域,即使 checkscopeContext 被销毁了,但是 JavaScript 依然会让 checkscopeContext.AO(活动对象) 活在内存中,f 函数依然可以通过 f 函数的作用域链找到它,这就是闭包实现的关键

参考

木易杨前端进阶