作用域
作用域
作用域是什么
作用域 维护所有声明标识符的(变量)的查询,并且限制当前代码对这些标识符的访问权限。
- 维护变量的声明及查询。作用域查询分为 LHS、RHS(简单理解为赋值操作(=)的左边还是右边)
- LHS 寻找赋值的目标
- RHS 寻找赋值的源头,即寻找值
- 维护代码对变量的访问权限
注意点 LHS 和 RHS 非严格按照等号划分,而是按照作用(是_寻找赋值的目标_还是_寻找值_)来划分,如下代码所示
// LHS afunction fn(a) { // RHS console // RHS a console.log(a);}fn(1);此处调用 fn(1) 时不存在等号,但是还是存在 LHS:
RHS:寻找标识符 fnLHS:将 a 赋值为 1RHS:寻找标识符 console,并调动 log 方法RHS:寻找 标识符 aJavascript 作用域
尽管将 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';
// 报错 SyntaxErrorwith ({ 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
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); // Functionfunction 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(词法环境),ES6的文档介绍了三种环境 global environment、module environment、function environment。
通常词法环境会与特定的 ECMAScript 代码联系,例如 FunctionDeclaration、WithStatement、TryStatement 的 Catch,且类似代码每次执行都会有一个新的词法环境被创建出来,如下代码所示
function(){...} // FunctionDeclaration
width(){...} // WithStatement
try{...}catch(){...} // Catch词法环境有两大成员:Environment Record(环境记录),可能为 null 的 Outer Lexical Environment(外部词法环境引用)
Outer Lexical Environment
对书写的代码中,包围当前词法环境的外部词法环境引用,如下代码所示
// Global Lexical Environmentfunction 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(对象式环境记录)
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; // VariableDeclarationconsole.log(window.a); // 1
function b() {} // FunctionDeclarationconsole.log(window.b); // function b(){}
function* c() {} // GeneratorDeclarationconsole.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