Open
Description
JS深入浅出 - 作用域链
参考文章:
★ JavaScript深入之作用域链
你不知道的JavaScript】(三)执行上下文及其生命周期
★ JavaScript深入之词法作用域和动态作用域
1. 概念
当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的指针链表就叫做作用域链。
作用域链保存的是指向执行上下文变量对象引用地址的指针,而不是保存变量对象的具体地址。
2. 作用域
要了解 JS 的作用域链,首先要知道什么是作用域。
- 作用域是指程序源代码中定义变量的区域,此外它还作为一套规则,规定了如何查找变量,也就是确定当前执行代码对变量的访问权限。
作用域按性质分为两种:
- 词法作用域,又称静态作用域。
- 动态作用域。
JavaScript 中采用词法作用域。
作用域在 JS 中又分为三种:
- 全局作用域
- 有且只有一个。
- 定义在全局作用域中的变量可在代码任何地方被访问(与作用域链有关)。
- 函数作用域
- 函数定义时确定的变量区域,其内部定义的变量只能在该函数内被调用。
- 块级作用域(ES6 新增)
- 任何代码块内部(一对花括号包裹)都会创建一个块级作用域。
- 块级作用域中有暂时性死区,变量禁止重复声明等特点。
词法作用域(lexical scoping)
词法作用域特点在于:任何变量和函数的作用域范围在书写代码时就被确定。
而动态作用域需要在变量或函数被调用时才能确定。
var value = 1;
function foo() {
console.log(value);
}
function bar() {
var value = 2;
foo();
}
bar();
- 上述代码中,若采用词法作用域,最终输出结果为
1
。
过程分析:
首先调用bar()
,执行内部的foo()
,遇到console.log(value);
语句,由于在foo()
的块级(词法)作用域中没有找到声明变量,因此沿着作用域链向外查找(查找过程后续细讲),此时foo()
的外层(词法)作用域为全局作用域,因此value === 1
。 - 若采用动态作用域,最终输出结果为
2
。 - 过程分析:
首先调用bar()
,执行内部的foo()
,遇到console.log(value);
语句,由于在foo()
的块级(动态)作用域中没有找到声明变量,因此沿着作用域链向外查找,此时由于foo()
是在bar()
作用域中调用的,因此foo()
的外层(动态)作用域为bar()
的作用域,因此value === 2
。
相比动态作用域,词法作用域更方便开发者查找分析,我们只需要看调用的函数在代码中定义的位置,即可清晰判断各层的作用域。
3. 作用域链的创建
函数定义阶段
函数有一个内部属性 [[scope]]
(数组),在函数定义时就自动生成,其保存了所有父级及以上层级的变量对象。换句话说,[[scope]]
就是当前函数下所有父变量对象的层级链,但是注意:[[scope]]
并不代表完整的作用域链!因为它没有添加当前执行上下文的变量对象。
function foo() {
function bar() {
...
}
}
函数创建时,函数各自的[[scope]]为:
foo.[[scope]] = [
globalContext.VO
];
bar.[[scope]] = [
fooContext.AO,
globalContext.VO
];
函数调用阶段
当函数被调用时,JS 引擎开始创建函数执行上下文,该阶段作用域链才真正的生成:
在创建完变量对象后,JS 会将变量对象添加到 [[scope]]
的前端,生成一条作用域链并保存为 Scope
。
Scope = [VO].concat([[scope]])
至此作用域链创建完毕,当进入函数上下文执行阶段时,VO 会被激活为 AO,此时作用域链通过代码可表示为:
Scope = [AO, ...[[scope]]]