作用域和词法环境
作用域
作用域是什么
「作用域」 维护所有声明标识符的(变量)的查询,并且限制当前代码对这些标识符的访问权限。
- 维护变量的声明及查询。作用域查询分为 LHS、RHS(简单理解为赋值操作(=)的左边还是右边)
- 「LHS」 寻找赋值的目标
- 「RHS」 寻找赋值的源头,即寻找值
- 维护代码对变量的访问权限
「注意点」 LHS 和 RHS 非严格按照等号划分,而是按照作用(是寻找赋值的目标还是寻找值)来划分,如下代码所示
// LHS a
function fn(a) {
// RHS console
// RHS a
console.log(a);
}
fn(1);
此处调用 fn(1) 时不存在等号,但是还是存在 LHS:
RHS:寻找标识符 fn
LHS:将 a 赋值为 1
RHS:寻找标识符 console,并调动 log 方法
RHS:寻找 标识符 a
Javascript 作用域
❝尽管将 Javascript 归为解释性语言,但事实上它是一门编译性语言。一般编译性语言分为如下几个阶段:词法解析 => 语法解析 => 代码生成 。Javascript 与常规编译性语言不同的是,它的编译时间并不长,通常为执行前的几毫秒甚至更少,然后通过其他手段(例如 JIT)来提高性能
❞
作用域主要包含两种工作模型:词法作用域、动态作用域
- 「词法作用域(静态作用域)」 在词法解析阶段定义的作用域。即是由代码的书写顺序来决定作用域。
- 「动态作用域」 运行时根据程序的流程信息来动态确定作用域。有没有觉得很像 this,但需要注意,Javascript 是没有动态作用域的,而 this 表现很像动态作用域
欺骗词法
Javascript 具有欺骗词法的手段,with、eval(不建议这样玩,当个老老实实的程序员)
eval
eval('var a = 2');
「非严格模式」
在非严格模式下,eval 在「当前作用域」插入变量
eval('var a = 3');
console.log(a); // 3
「严格模式」 在严格模式下,eval 重新创建作用域
'use strict';
eval('var a = 3');
console.log(a); // 报错
「除了 eval 以外,也可以使用 setTimeout 和 setInterval 也可以插入字符串来执行」
const str = 'console.log(1)';
setTimeout(str, 1000); // 1
非常不建议使用!
with
「严格模式下 with 无法使用」
'use strict';
// 报错 SyntaxError
with ({ name: 'keven' }) {
}
with 引入对象,将对象处理为一个新的词法作用域,但 with 内部的 var 变量声明「不会保留在该块级作用域」,而是「保留在 with 的作用域」。(注意噶,是 var 声明,但 let、const 声明是保存在块级作用域的)
Javascript 作用域分类
「全局作用域」
每个执行环境只有一个全局作用域,浏览器是 window,node.js 为 global
window.x = 1;
global.x = 1;
「函数作用域」
每一个函数会单独创建函数作用域
function fn() {
let x = 1;
}
「块级作用域」
ES6 中增加块级作用域,在 {} 中以 let、const 声明的变量存在块级作用域
{
let x = 1;
}
「中间作用域」
ES6 中增加中间作用域,在 「函数」且「函数存在默认值」时,函数的默认值即为中间作用域
请注意这个作用域,标准里并不含该作用域。如果想详细地了解该作用域,请点击[ES6: Default values of parameters](http://dmitrysoshnikov.com/ecmascript/es6-notes-default-values-of-parameters/#conditional-intermediate-scope-for-parameters 'ES6: Default values of parameters' "ES6: Default values of parameters")
function fn(x = 1) {
console.log(x); // 1,如果 x 无默认值,则是 undefined
var x = 2; // 不要用let、const去试哈,由于TDZ关系,会报错
console.log(x); // 2
}
作用域嵌套
把作用域看成一个气泡,父级作用域是一个大气泡,包裹着子作用域的小气泡,全局作用域是一个最大的气泡,包裹其他的气泡。
当开始寻找变量时,从自身作用域开始,逐级向上查找,直到全局作用域
window.x = 1;
function p() {
function c() {
x = 3;
}
}
作用域提升
上面提到了在 Javascript 执行前,存在编译过程(预解析),在此过程中会解析「函数声明」、「变量声明」。
对于变量声明来说,是将 定义声明 和 赋值声明 分开的
var a = 1;
// 可以翻页为
var a; // 定义声明
a = 1; // 赋值声明
「优先顺序」:函数声明提升「优先于」变量声明
console.log(fn); // Function
function fn(params) {}
var fn = 2;
TDZ
「TDZ(Time Dead Zone)」 在 ES6 中,对 let、const 声明时,TDZ 使无法在 let、const 赋值语句前访问该变量。
闭包
「闭包」 函数可以记住、并访问原作用域,即使不在原作用域运行(在非原作用域的地方,但可以访问原作用域变量的功能)
function p() {
const a = 1;
return function c() {
console.log(a);
};
}
const c = p();
c(); // 此时c处于全局作用域,但任然可以访问变量a
// [[Scopes]]; 可以打印下 p.prototype,里面存在 [[ Scopes ]] 属性,可以查看作用域
IIFE(立即调用函数表达式)
对于自执行函数,按照闭包的定义,其实 IIFE 严格来说不属于闭包,因为在 IIFE 函数中,并「不是在当前作用域以外的地方执行的」
(function () {
let x = 1;
})(); // 它就是在当前作用域执行的,而不是在外部作用域执行的
Lexical Environment
在 ES5 后,Scope 被替代为 Environment,Environment 取代了作用域,称为 「Lexical Environment(词法环境)」。
通常词法环境会与特定的 ECMAScript 代码联系,例如 FunctionDeclaration、WithStatement、TryStatement 的 Catch,且类似代码每次执行都会有一个新的词法环境被创建出来,如下代码所示
function(){...} // FunctionDeclaration
width(){...} // WithStatement
try{...}catch(){...} // Catch
词法环境有两大成员:「Environment Record(环境记录)」,可能为 null 的 「Outer Lexical Environment(外部词法环境引用)」

Outer Lexical Environment
对书写的代码中,包围当前词法环境的外部词法环境引用,如下代码所示
// Global Lexical Environment
function a() {
// Function Lexical Environment
function b() {
// Function Lexical Environment
}
}
分析(以下代码不考虑函数运行时机)
- 进入代码,创建全局词法环境
- 向下执行,读到 a 函数声明,则创建 A 函数词法环境,且 Outer 指向全局词法环境
- 继续向下执行,读到 b 函数声明,则创建 B 函数词法环境,且 Outer 指向 A 函数词法环境

我们看向「Function Lexical Environment(函数词法环境)」,函数词法环境还可以建立一个新的this 绑定(BindThisValue(V)),但此处不是主要分析 this,所以这里就只是说明一下
值得一提的时,在 ES6 后,Javascript 有了自己的模块规范 ES Module,如下代码所示
import { a } from xx;
那么问题来了,a 存在于那个词法环境?全局词法环境?函数词法环境?这俩咋看咋都不太合适。在 ES6 文档规定了词法环境 「Module Lexical Environment(模块词法环境)」,模块词法环境的外部词法引用指向全局词法环境
开始说了外部词法环境引用可能为 null,那么不妨猜猜,为什么会存在 null?此时我们再看一下全局词法环境,由于全局词法环境是“最大的、最外层的”,不会存在“更外的”词法环境了,那么必然的它的外部词法环境引用为 null
Environment Record
记录在其关联的词法环境范围内创建的标识符绑定。
规范中主要有两种主要的环境记录:「Declarative Environment Records(声明式环境记录)」 、「Object Environment Records(对象式环境记录)」(文档后头还搞了个 Global Environment Record)
Environment Records 是一个抽象类,存在三个具体的子类,「Declarative Environment Record」 ,「Object Environment Record」,「Global Environment Record(全局环境记录)」
Global Environment Record
表示所有 ECMAScript 共享的最外部范围在通用领域中处理的脚本元素。全局环境记录提供了内置全局变量的绑定,全局对象的属性以及所有顶级声明的绑定
全局环境记录存在三个属性,如下表格所示
| Field Name | Value | Meaning |
|---|---|---|
| [[ObjectRecord]] | 对象环境记录 | 绑定对象为全局对象,包含 FunctionDeclaration, GeneratorDeclaration, and VariableDeclaration |
| [[DeclarativeRecord]] | 声明环境记录 | 绑定在全局代码中的所有声明,但去除 FunctionDeclaration, GeneratorDeclaration, and VariableDeclaration |
| [[VarNames]] | 字符串数组 | 由 FunctionDeclaration, GeneratorDeclaration, 、VariableDeclaration 在全局对象绑定的标识符数组 |
举个例子,如下代码所示
var a = 1; // VariableDeclaration
console.log(window.a); // 1
function b() {} // FunctionDeclaration
console.log(window.b); // function b(){}
function* c() {} // GeneratorDeclaration
console.log(window.c); // function *c(){}
let d = 1; //
console.log(window.d); // undefined
这里值得一提的是,请注意 let/const 声明,它属于 Let and Const Declarations(「LexicalDeclaration」),而不是 Variable Statement(「VariableDeclaration」),所以在某些地方表现和 var 是不一致的,例如在全局声明时,let/const 并不会挂载于 window 对象上
ecma-262/6.0 sec-declarations-and-the-variable-statement
Declarative Environment Records
记录那些将标识符与ECMA 值直接相绑定,例如 variable, constant, let, class, module, import, function declarations ...
var a = 1; // variable
function b() {} // function declarations
let c = 2; // let
try{}catch(){ var b = 2; }
「Function Environment Records(函数环境记录)」、「Module Environment Records(模块环境记录)」 是声明性环境记录的子类
Object Environment Records**
记录那些将标识符与具体对象的属性绑定,例如 With 表达式。如下代码所示
var obj = { foo: 42; };
with (obj) {
foo = foo / 2;
}
console.log(obj);
值得一提的是,每个对象环境记录都与一个称为其绑定对象的对象相关联,例如全局环境记录的[[ObjectRecord]]属性,就与 window 对象关联(只说浏览器噶)
根据 Environment 解释 Scope
为什么作用域可以向外层查询
由于每个词法环境的 Outer 记录了外层词法环境的引用,当在自身词法环境记录无法寻找到该标识符时,可以根据 Outer 向外层寻找,直到 null(有木有觉得很像[[Prototype]],自身寻找不到属性,则沿着[[Prototype]]查找,直到 null)
LHS 和 RHS
| Field Name | Method | mutable binding |
|---|---|---|
| LHS | SetMutableBinding(N,V, S) | 设置环境记录已存在的「可变绑定」的值 |
| RHS | GetBindingValue(N,S) | 返回环境记录中已经存在绑定记录的值 |
TDZ
环境记录有两个方法 CreateMutableBinding(N,D),InitializeBinding(N,V)
CreateMutableBinding(N, D):在环境记录中创建可变绑定记录,但未初始化
InitializeBinding(N,V):给在环境记录中已存在,但未初始化的绑定初始化并赋值
由此可知,我们的声明一个变量其实是需要两个步骤,1. 在词法环境创建绑定记录;2. 初始化并赋值
「对于 var 来说」
- 在环境激励创建可变绑定记录
- 立刻初始化,并赋值为 undefined
「对于 let 来说」
- 在词法环境创建可变绑定记录
- 虽然确实创建了变量,但未初始化
根据 GetBindingValue、SetMutableBinding 方法的说明文档,如果当前变量未初始化,则抛出 ReferenceError
❝The variables are created when their containing Lexical Environment is instantiated but may not be accessed in any way until the variable’s LexicalBinding is evaluated
❞
由上可知,let/const 不会变量提升其实是错误的,因为它确实已经创建了变量,只是未初始化,导致你无法使用而已
「参考文献」
《你不知道的 Javascript 上卷》
ecma-262 6th
本文使用 mdnice 排版