作用域

○ 作用域(scope)是代码层面的概念,是代码被划分成的一个个区域,用来约定位于不同区域变量的可访问特性。根据所在位置,作用域可分为全局作用域和局部作用域(如函数作用域、块级作用域)。
○ 块级作用域通过在{}里使用 let 或 const 声明变量实现。
○ 函数作用域通过函数体划分。
○ 全局作用域是不在任何函数作用域或块级作用域中的区域。

作用域链

● 由于块和函数体可以嵌套,使作用域也可以存在上下层嵌套关系,上下层关系具有称为“作用域链(scope chain)”的机制。JS 约定作用域链有如下特性:
(1)在每个局部作用域内可以向上访问上层局部作用域直到全局作用域中的变量,但不能向下访问嵌套函数或嵌套块内的变量。
(2)若在局部作用域使用一个变量在局部不存在,则不断向上层查找直至找到或在全局作用域创建(非严格模式且找不到时)。
(3)若在局部作用域声明一个与上层变量同名的变量,则在局部作用域内变量值为作用域内的值,而在上层作用域同名变量仍是上层作用域内的值。
● 练习:

1
2
3
4
5
6
7
8
9
var value = 1;
function foo() {
console.log(value);// foo()函数声明在全局作用域,向上查找到value在全局作用域
}
function bar() {
var value = 2; // 局部同名value变量,不影响全局value变量值
foo(); // 向上查找到foo()函数在全局作用域
}
bar(); // 打印 1

● Chrome 开发者工具可以查看作用域链。以上述“练习”为例,当调用栈(Call Stack)执行到 foo()函数内部时,能看到 foo()的作用域链如下图 1。如果将 foo()的定义移动在 bar()内部,作用域链将变成图 2 的样子。

图 1 图 2

词法环境

词法环境(lexical environment)机制实现了作用域的特性。
◆ 每个作用域都对应一个词法环境,词法环境在相应作用域的代码被评估(evalute)的时候就被创建,就是下文会提到的“执行上下文的创建阶段”。
◆ 词法环境中除了存储当前作用域声明的变量和函数,还有个“外部词法环境”取值为上层作用域的词法环境的引用,由此实现作用域链的建立。

执行上下文

□ 执行上下文(execution context)是 JS 代码执行涉及的状态信息的统称。
□ 根据代码段的位置,可将执行上下文可分为三种类型:(1)全局执行上下文,在所有代码还未执行前创建(2)函数执行上下文,函数调用时创建(3)eval 执行上下文,执行 eval 语句时创建。
□ “块级执行上下文”是不存在的(stackoverflow 问答),执行块级代码时仍然在当前的执行上下文进行。
□ 所有的执行上下文存储在执行上下文栈(execution context stack),也称执行栈,也称为调用栈。随着代码从全局入口执行到调用函数到函数返回,新的执行上下文也不断被创建、入栈、切换执行、出栈、销毁,当前正在执行的代码段对应的执行上下文永远在栈顶。此过程可以借助代码执行可视化工具直观了解。
□ 每个执行上下文都包含创建阶段(creation phase)与执行阶段(execution phase),执行阶段才真正执行代码。
创建阶段会做如下事情:
(1)创建执行上下文,创建词法环境并与执行上下文关联。
(2)代码段区域中声明的变量,为其分配空间,如果是 var 声明则赋值 undefined,否则不赋值。
(3)代码段区域中声明的函数,将其载入内存。
(4)将变量和函数记录到词法环境。
□ 创建阶段对声明变量与函数的处理实现了“声明提前(hoisting)”。let 和 const 声明的变量也实现了声明提前,只是不能提前使用,这一点可以通过提前访问变量产生的报错信息差异验证
□ 练习:

1
2
3
4
5
6
var v = "yoyo";
(function(){
console.log(v); // 输出“undefined”,局部变量v声明提前,与上层同名变量使用局部值
var v = "check now";
console.log(v); // 输出“check now”
})();

□ 另一个代码执行可视化工具(支持 ES6),支持 Python、C 等其他语言。

上下文

■ 上下文(context)通常说的就是 this 。
■ this 存在于所有的执行上下文中,this 的取值取决于其所处的执行上下文,具体规则如下:
(1)在全局执行上下文,this 值指向全局对象(非 strict 模式下,在浏览器环境就是 window )。
(2)在函数执行上下文,this 值指向调用当前函数的对象。
(3)eval 执行上下文的 this 值还与是 direct eval 还是 indirect eval有关,对于 direct eval,this 值与 eval 语句所在执行上下文的 this 一致;对于 indirect eval,等同于 eval 语句在全局执行上下文,此时 this 值指向全局对象。
■ 练习 1

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
var a = 20;
var obj = {
a: 10,
c: this.a + 20,
fn: function () {
return this.a;
}
}

class A {
constructor() {
this.a = 10;
}
fn() {
return this.a;
}
fn2 = () => {
return this.a;
}
}
var objA = new A();

console.log(obj.c); // 40,c属性的值在执行完赋值语句之后就确定了,计算其值时处于全局执行上下文
console.log(obj.fn()); // 10
var oneFun = obj.fn; // oneFun的值是obj.fn的函数体
console.log(oneFun()); // 20
oneFun = objA.fn;
console.log(oneFun()); // Uncaught TypeError: Cannot read properties of undefined。
oneFun = objA.fn2;
console.log(oneFun()); // 10

其中“objA.fn”在全局执行上下文中执行时 this 取值为 undefined,原因是class 的 body 强制在 strict 模式下执行,无论是否显式指定”use strict”。

■ 练习 2

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

var foo = {
value: 2,
bar: function() {
return this.value;
},
};

/* 表达式结果是foo.bar的函数体,函数体在全局执行上下文执行 */
console.log((foo.bar = foo.bar)()); // 1
console.log((false || foo.bar)()); // 1
console.log((foo.bar, foo.bar)()); // 1

参考文献

JavaScript Execution Context and Hoisting
Understanding Execution Context in JavaScript
Understanding Scope and Context in JavaScript
What is the Difference Between Scope and Context in JavaScript
Javascript: Execution Context and Call Stack
ECMA262 标准
(1, eval)(‘this’) vs eval(‘this’) in JavaScript