前言
ECMAScript标准是深入学习JavaScript原理最好的资料,没有其二。
通过增加对ECMAScript语言的理解,理解javascript现象后面的逻辑,提升个人编码能力。
欢迎关注和订阅专栏 **[重学前端-ECMAScript协议上篇]
先来几个问题
-
什么是作用域
-
作用域是静态语义分析时就确定的, 还是运行时(代码执行时)确定的
-
下面的
varA
和varB
的查找有区别嘛inijavascript 复制代码 var varA = 'varA'; ; (function fn1() { (function fn2() { (function fn3() { console.log(varA); console.log(varB); })(); })(); })();
当提到作用域时,都是和什么关联的,当然是标识符啊,也就是开发者们常说的变量名。
之前的章节,得知,标识符的绑定关系,都是存在哪的呢?
答:是环境记录。
标识符的查找是由谁发起的呢?
答:是执行上下文。
所以,一切的根源还得从标识符开始,再到环境记录,执行上下文。
标识符
标识符是怎么产生的呢? 最为常见的一种方式是申明语句, 申明语句分为四类,可以从 14 ECMAScript Language: Statements and Declarations 可以查看到

类别 | 说明 |
---|---|
VariableStatement | 使用var申明的语句 |
HoistableDeclaration | 可提升的申明,其实就是各种函数申明 FunctionDeclaration GeneratorDeclaration AsyncFunctionDeclaration AsyncGeneratorDeclaration |
ClassDeclaration | class的申明 |
LexicalDeclaration | 使用let和const的申明语句 |
php
javascript
复制代码
// VariableStatement
var varA = 'varA';
// FunctionDeclaration
function func (){};
// GeneratorDeclaration
function * funcGenerator (){}
// AsyncFunctionDeclaration
async function funcAsync () {}
// AsyncGeneratorDeclaration
async function * funcAsyncGenerator () {}
// ClassDeclaration
class ClassA {};
// LexicalDeclaration
let letA = 'letA';
const constA = 'constA';
当然也不是只有申明才会产生标志符
javascript
javascript
复制代码
try {
throw new Error('错就是错,不要反驳')
}catch(err){ // err 也是标志符
console.log("error:", err) // console也是标志符
}
function sum(num1, num2){ // num1 , num2 也是标志符
return num1 + num2
}
function test(){
console.log(arguments.length) // arguments 也是标志符
}
这四类申明语句,又可被分为 变量申明 和 词法申明。 在协议中分别有两个概念对相应, 更多细节可以参见 第四节 <<4.语法导向操作>>
以及协议TopLevel的 (Script顶层代码和函数顶层代码)
- 8.2.8 Static Semantics: TopLevelLexicallyDeclaredNames 词法申明名
- 8.2.10 Static Semantics: TopLevelVarDeclaredNames 变量申明名
两种情况下,函数申明是有区别的,记住后面要考的:
词法申明名和变量申明名 与 四种申明 的关系如下
VariableStatement var 申明 | HoistableDeclaration 可提升的申明(函数)非顶层情况 | ClassDeclaration class申明 | LexicalDeclaration let/const申明 | |
---|---|---|---|---|
LexicallyDeclaredNames | ✅ | ✅ | ✅ | |
VarDeclaredNames | ✅ |
VariableStatement var 申明 | HoistableDeclaration 可提升的申明(函数) | ClassDeclaration class申明 | LexicalDeclaration let/const申明 | |
---|---|---|---|---|
TopLevelLexicallyDeclaredNames | ✅ | ✅ | ||
TopLevelVarDeclaredNames | ✅ | ✅ |
比如下面两种场景一个是Script的顶层代码,一个是处于 Block块的代码。
VarDeclaredNames 的结果 :["varA"
, "funcA"
]。
场景一: 顶层代码
xml
javascript
复制代码
<script>
"use strict"
var varA = "varA";
const constA = "constA";
let letA = "letA";
function funcA(){};
class ClassA {};
</script>
执行上下文,环境记录和标志符标定关系如下:

场景二: 代码块
在块内执行 VarDeclaredNames 的结果 :["varA"
],此时的函数申明 funcA
属于词法申明队列了。
xml
javascript
复制代码
<script >
"use strict"
{
var varA = "varA";
const constA = "constA";
let letA = "letA";
function funcA(){};
class ClassA {};
}
</script>
执行上下文,环境记录和标志符标定关系如下:

那为什么要去区分是词法申明还是变量申明呢? 这会影响执行上下文标志符的查找,因为标志符的绑定关系大都是从词法申明开始查找的。词法环境记录和变量环境记录也可以是同一个环境记录, 比如全局代码执行的执行时,词法环境记录就等于变量环境记录,都指向全局环境记录。
环境记录
标识符会在执行上下文的环境记录上,创建一个绑定关系,之后需要用到时时候, 再通过标识符查找绑定关系,进而进行取值等操作。除此之外,标志符还可以更改绑定关系,删除绑定关系。这不就是典型的 增,删,改,查嘛。
下面就是所有环境记录都具备的方法,

函数环境记录 和 模块环境记录是申明环境记录的子类。 而全局环境记录,又是由 对象环境记录和申明环境记录组成。
环境记录 | 说明 | 用途 |
---|---|---|
Declarative Environment Record | 申明环境记录 | 最为常见。参数绑定,块级作用域变量绑定等 |
Function Environment Record | 函数环境记录函数调用准备阶段时会创建。 | 函数顶级代码里面的变量绑定或者作为连接外部环境记录的桥梁。函数的调用部分会详细说明,这个环境记录可能什么都不存。 |
Module Environment Record | 模块环境记录。模块初始化时创建。 | 绑定模块顶层代码的标识符 |
Object Environment Record | 对象环境记录。with 和 和全局代码执行时会创建。 | 绑定with 或者 全局环境记录标志符 |
Global Environment Record | 全局环境记录。申明环境记录 + 对象环境记录。 | 绑定全局顶层代码的标识符。 |
标识符有
- 实例化 CreateMutableBinding(N, D) , CreateImmutableBinding(N, S)
- 初始化 InitializeBinding(N, V)
两种操作,创建表示有这个标识符的名称了,但是没有绑定值, 从代码层面理解,可以理解为
- 实例化 = 申明
- 初始化 = 设置值
下面从一段简单的全局顶层代码,一起来理解标识符绑定的创建,实例化,以及查找。
全局顶层代码示例
示例代码如下:
ini
html
复制代码
<script>
var varA = 'varA';
const constA = 'constA';
{
var varA = 'block_varA';
let constA = 'block_constA';
}
console.log(varA, constA);
</script>
代码执行的基本流程
整个代码的基本执行流程是(忽略亿点点细节)
-
16.1.5 ParseScript ( sourceText, realm, hostDefined )
script 的源码转为 脚本记录 Script Record,script标签转为解析节点。
-
16.1.6 ScriptEvaluation ( scriptRecord )
- 16.1.7 GlobalDeclarationInstantiation ( script, env )
实例化全局变量和函数声明(函数申明和var申明会初始化) - Evaluation of script
全局顶层代码执行。
- 16.1.7 GlobalDeclarationInstantiation ( script, env )
ParseScript
ParseScript 会对源码解析,返回脚本记录。 其中最为重要的就是[[ECMAScriptCode]]
, 即源码对应的解析树。

\[ECMAScriptCode\]\] 大致的内容如下: ```json javascript 复制代码 { "type": "Script", "strict": false, "ScriptBody": { "type": "ScriptBody", "strict": false, "StatementList": [{ "type": "VariableStatement", "strict": false, "VariableDeclarationList": [{ "type": "VariableDeclaration", "strict": false, "BindingIdentifier": { "type": "BindingIdentifier", "strict": false, "name": "varA" }, "Initializer": { "type": "StringLiteral", "strict": false, "value": "varA" } }] }, { "type": "LexicalDeclaration", "strict": false, "LetOrConst": "const", "BindingList": [{ "type": "LexicalBinding", "strict": false, "BindingIdentifier": { "type": "BindingIdentifier", "strict": false, "name": "constA" }, "Initializer": { "type": "StringLiteral", "strict": false, "value": "constA" } }] }, { "type": "Block", "strict": false, "StatementList": [{ "type": "VariableStatement", "strict": false, "VariableDeclarationList": [{ "type": "VariableDeclaration", "strict": false, "BindingIdentifier": { "type": "BindingIdentifier", "strict": false, "name": "varA" }, "Initializer": { "type": "StringLiteral", "strict": false, "value": "block_varA" } }] }, { "type": "LexicalDeclaration", "strict": false, "LetOrConst": "let", "BindingList": [{ "type": "LexicalBinding", "strict": false, "BindingIdentifier": { "type": "BindingIdentifier", "strict": false, "name": "constA" }, "Initializer": { "type": "StringLiteral", "strict": false, "value": "block_constA" } }] }] }, { "type": "ExpressionStatement", "strict": false, "Expression": { "type": "CallExpression", "strict": false, "CallExpression": { "type": "MemberExpression", "strict": false, "MemberExpression": { "type": "IdentifierReference", "strict": false, "escaped": false, "name": "console" }, "IdentifierName": { "type": "IdentifierName", "strict": false, "name": "log" }, "PrivateIdentifier": null, "Expression": null }, "Arguments": [{ "type": "IdentifierReference", "strict": false, "escaped": false, "name": "varA" }, { "type": "IdentifierReference", "strict": false, "escaped": false, "name": "constA" }] } }] } } ``` 不好查看,没关系,用节点树画出来, 稍微注意一下 红色标注 `BindingIdentifier`节点,对应的就是标志符的名称对应的节点。  ### 解析变量申明和词法申明 [GlobalDeclarationInstantiation ( script, env )](https://link.juejin.cn/?target=https%3A%2F%2Ftc39.es%2Fecma262%2F%23sec-globaldeclarationinstantiation "https://link.juejin.cn/?target=https%3A%2F%2Ftc39.es%2Fecma262%2F%23sec-globaldeclarationinstantiation") 会对整个解析树进行静态分析,提取标识符名和创建标识符绑定关系。 [GlobalDeclarationInstantiation ( script, env )](https://link.juejin.cn/?target=https%3A%2F%2Ftc39.es%2Fecma262%2F%23sec-globaldeclarationinstantiation "https://link.juejin.cn/?target=https%3A%2F%2Ftc39.es%2Fecma262%2F%23sec-globaldeclarationinstantiation") 的整个流程在 《script加载和全局顶层代码申明实例化》章节有详细过程。 标识符创建和绑定是在**静态语义分析**的时候处理的,还没有到真正的执行代码。 申明分为两类: * 变量申明 VarDeclaredNames: var 申明,顶层代码的函数申明 * 词法申明 LexicallyDeclaredNames:let/const/ class 申明,非顶层的函数申明 #### VarDeclaredNames 获取变量申明的过程,你可以理解为就是检查节点上的申明和语句,匹配特定节点类型 * VariableStatement * VariableDeclaration * ExportDeclaration * CaseBlock * 等等 然后按照规律去获取对应节点标志符的名称。本示例就是找 `BindingIdentifier`节点 name 的值。 **本示例的返回值是 两个 \[** `varA` **,** `varA` **\]。** 大致的逻辑用代码表示如下: 而本示例属于 传入的node 就是 type 为 Script的根节点。 ```kotlin javascript 复制代码 function VarDeclaredNames(node) { if (isArray(node)) { const names = []; for (const item of node) { names.push(...VarDeclaredNames(item)); } return names; } switch (node.type) { case 'VariableStatement': return BoundNames(node.VariableDeclarationList); case 'VariableDeclaration': return BoundNames(node); case 'IfStatement': { const names = VarDeclaredNames(node.Statement_a); if (node.Statement_b) { names.push(...VarDeclaredNames(node.Statement_b)); } return names; } case 'Block': return VarDeclaredNames(node.StatementList); case 'WhileStatement': return VarDeclaredNames(node.Statement); case 'DoWhileStatement': return VarDeclaredNames(node.Statement); case 'ForStatement': { const names = []; if (node.VariableDeclarationList) { names.push(...VarDeclaredNames(node.VariableDeclarationList)); } names.push(...VarDeclaredNames(node.Statement)); return names; } case 'ForInStatement': case 'ForOfStatement': case 'ForAwaitStatement': { const names = []; if (node.ForBinding) { names.push(...BoundNames(node.ForBinding)); } names.push(...VarDeclaredNames(node.Statement)); return names; } case 'WithStatement': return VarDeclaredNames(node.Statement); case 'SwitchStatement': return VarDeclaredNames(node.CaseBlock); case 'CaseBlock': { const names = []; if (node.CaseClauses_a) { names.push(...VarDeclaredNames(node.CaseClauses_a)); } if (node.DefaultClause) { names.push(...VarDeclaredNames(node.DefaultClause)); } if (node.CaseClauses_b) { names.push(...VarDeclaredNames(node.CaseClauses_b)); } return names; } case 'CaseClause': case 'DefaultClause': if (node.StatementList) { return VarDeclaredNames(node.StatementList); } return []; case 'LabelledStatement': return VarDeclaredNames(node.LabelledItem); case 'TryStatement': { const names = VarDeclaredNames(node.Block); if (node.Catch) { names.push(...VarDeclaredNames(node.Catch)); } if (node.Finally) { names.push(...VarDeclaredNames(node.Finally)); } return names; } case 'Catch': return VarDeclaredNames(node.Block); case 'Script': if (node.ScriptBody) { return VarDeclaredNames(node.ScriptBody); } return []; case 'ScriptBody': return TopLevelVarDeclaredNames(node.StatementList); case 'FunctionBody': case 'GeneratorBody': case 'AsyncBody': case 'AsyncGeneratorBody': return TopLevelVarDeclaredNames(node.FunctionStatementList); case 'ClassStaticBlockBody': return TopLevelVarDeclaredNames(node.ClassStaticBlockStatementList); case 'ExportDeclaration': if (node.VariableStatement) { return BoundNames(node); } return []; default: return []; } } function TopLevelVarDeclaredNames(node) { if (isArray(node)) { const names = []; for (const item of node) { names.push(...TopLevelVarDeclaredNames(item)); } return names; } switch (node.type) { case 'ClassDeclaration': case 'LexicalDeclaration': return []; case 'FunctionDeclaration': case 'GeneratorDeclaration': case 'AsyncFunctionDeclaration': case 'AsyncGeneratorDeclaration': return BoundNames(node); default: return VarDeclaredNames(node); } } ``` #### LexicallyDeclaredNames 获取词法申明的过程,你可以理解为就是检查节点上的申明和语句,匹配特定节点类型 * ClassDeclaration (对应class) * LexicalDeclaration (对应let/const) 然后按照规律去获取对应节点标志符的名称。本示例就是找 `BindingIdentifier`节点 name 的值。 **这里有的同学认为是 \[** `constA` **,** `constA` **\], 实际上的值是 \[** `constA` **\]** ,**这个顶层代码解析过程不会遍历** `Block`**类型的节点。** 用代码表示大致如下: ```php javascript 复制代码 function LexicallyDeclaredNames(node) { switch (node.type) { case 'Script': if (node.ScriptBody) { return LexicallyDeclaredNames(node.ScriptBody); } return []; case 'ScriptBody': return TopLevelLexicallyDeclaredNames(node.StatementList); case 'FunctionBody': case 'GeneratorBody': case 'AsyncBody': case 'AsyncGeneratorBody': return TopLevelLexicallyDeclaredNames(node.FunctionStatementList); case 'ClassStaticBlockBody': return TopLevelLexicallyDeclaredNames(node.ClassStaticBlockStatementList); default: return []; } } function TopLevelLexicallyDeclaredNames(node) { if (isArray(node)) { const names = []; for (const StatementListItem of node) { names.push(...TopLevelLexicallyDeclaredNames(StatementListItem)); } return names; } switch (node.type) { case 'ClassDeclaration': case 'LexicalDeclaration': return BoundNames(node); default: return []; } } ``` ### 创建标识符绑定 这一步也发生在 [GlobalDeclarationInstantiation ( script, env )](https://link.juejin.cn/?target=https%3A%2F%2Ftc39.es%2Fecma262%2F%23sec-globaldeclarationinstantiation "https://link.juejin.cn/?target=https%3A%2F%2Ftc39.es%2Fecma262%2F%23sec-globaldeclarationinstantiation") ,整个流程在 《script加载和全局顶层代码申明实例化》章节有详细过程。 从上面的解析变量申明和词法申明,得知结果: * VarDeclaredNames : \[`varA`, `varA`
- LexicallyDeclaredNames: [
constA
]
虽然这里 变量申明有两个 varA
,但是在申明实例化的第9和第10步,会去重,所以最后只会创建一个绑定关系。(varDeclarations 是 和 VarDeclaredNames 对应的,只不过是解析节点)

GlobalDeclarationInstantiation ( script, env ) 中的 env 参数就是全局环境记录,而整个全局顶层代码申明实例化过程,所有的词法申明和变量申明的绑定关系都是在 env上创建的。 相比函数申明实例化,是简单太多了。
全局环境记录又是由 对象环境记录 和 申明环境记录组成的, 词法申明和变量申明对应的绑定关系如下。
VarDeclaredNames | LexicallyDeclaredNames | |
---|---|---|
创建方法 | env.CreateGlobalVarBinding(var)env.CreateGlobalFunctionBinding (function) | env.CreateMutableBinding (let/class)env.CreateImmutableBinding(const) |
对象环境记录 | ✅ | |
申明环境记录 | ✅ |
说明:
-
变量申明:var 申明 和 函数申明的绑定关系虽然都在 对象环境记录上,但又是不一样的。
- var 申明会被实例化和初始化,只不过初始化的值 是 undefined.
- 函数申明是会初始化值为函数对象 。细节在GlobalDeclarationInstantiation ( script, env ) 16步骤
下面的 log.name 是能有效输出的,因为函数对象(function object)在代码执行前就已经初始化好了。
javascript
javascript
复制代码
console.log(varA); // undefined
console.log(log.name); // log
function log(data){
console.log(data);
}
var varA = 'varA';
- 词法申明: let 和 const 区别在于一个可变,一个不可变。 两者在申明实例化完毕后,都是没有被初始化的或者说被设置值的。
所以执行完毕 GlobalDeclarationInstantiation ( script, env ) 之后,重申一遍,一行全局顶层代码都还没执行,正在执行上下文,环境记录,标识符绑定关系如下:

细心的同学发现了, GlobalThisValue
的属性上已经也有了 varA
, 这是因为在 对象环境记录上绑定属性的时候 就是在 在全局对象上 绑定属性。
回归对象环境记录字段,这个 [[BindingObject]]
就是 全局对象, 也是标志符绑定的对象。

当然 通过 globalThis.varA
和 直接 varA
两种方式,标识符的查找路径是不一样的。
globalThis.varA
走的是 13.3.2 Property Accessors 属性获取逻辑,会判断varA
是不是属性引用,如果是直接就在对象上取值varA
就是从执行上下文的词法环境记录开始找
到此,全局顶层代码的申明实例化完毕,varA
和 constA
都只是申明绑定关系,并没有设置值的。
代码执行
先提个问题:
当示例代码,执行到 console.log(varA, constA);
这句代码时, 请问 varA
和 constA
这两个东西,到底是什么?
ini
html
复制代码
<script>
var varA = 'varA';
const constA = 'constA';
{
var varA = 'block_varA';
let constA = 'block_constA';
}
console.log(varA, constA);
</script>
那 var varA = 'varA';
这个 VariableDeclaration 表达式是怎么执行的呢?
VariableDeclaration 的执行 ,协议在 14章的 14.3.2.1 Runtime Semantics: Evaluation 有定义,其由很多种场景,var varA = 'varA';
对应如下场景

简单整理
- 通过标识符
varA
查找到对应的引用记录 - 评估
Initializer
,获取其值 - 再通过varA的引用记录 给 varA标志符绑定值
上面的提到的Initializer
就是 函数申明对应的VariableDeclaration
节点 的子节点。

此处变量申明的逻辑
- 通过
BindingIdentifier
找到 标志符varA
的引用记录, - 执行
Initializer
,获得值,再通过varA
对应的引用记录 设置值
当然因为ES6带来了解构赋值等,所以变量申明也多了新的形式,用伪代码编写如下:
var varA = 'varA'
走的是 BindingIdentifier
和 Initializer
都有的逻辑
scss
javascript
复制代码
function* Evaluate_VariableDeclaration({ BindingIdentifier, Initializer, BindingPattern }: ParseNode.VariableDeclaration) {
// 如果有绑定标志符节点
if (BindingIdentifier) {
if (!Initializer) {
// 没有舒初始化值的逻辑, 比如 var varA;
return NormalCompletion(undefined);
}
// 取标志符名,本示例: varA
const bindingId = StringValue(BindingIdentifier);
// 执行上下文,查找绑引用记录
const lhs = Q(ResolveBinding(bindingId, undefined, BindingIdentifier.strict));
// 如果是匿名的函数名字
let value;
if (IsAnonymousFunctionDefinition(Initializer)) {
// 这一步是 实例化函数对象,
// 对应场景 var a = function (){}; 虽然函数是匿名函数,但是 a.name 是有值的
value = yield* NamedEvaluation(Initializer, bindingId);
} else {
// 评估初始表达式,获得引用记录 或者 值,
// 本示例是 值,不是引用记录, 如果是 varA = varB, rhs就是引用记录
const rhs = yield* Evaluate(Initializer);
// 取值, 因为本示例 rhs 是字符串值,直接返回
// 如果 rhs 引用记录,需要从环境记录取值
value = Q(GetValue(rhs));
}
// 通过左边的引用记录 给标志符标定值
return Q(PutValue(lhs, value));
}
// 对应 var {varA} = {varA: "varA"} 解构等其他情况;
const rhs = yield* Evaluate(Initializer);
// 2. Let rval be ? GetValue(rhs).
const rval = Q(GetValue(rhs));
// 3. Return the result of performing BindingInitialization for BindingPattern passing rval and undefined as arguments.
return yield* BindingInitialization(BindingPattern, rval, Value.undefined);
}
重点变成为
- lhs 是什么,rhs 是什么
- getValue(rref) 什么操作,返回结果是什么
- PutValue(lref , rval) 是什么操作
- lhs
lhs 是 执行上下文 通过标识符 varA 作为参数, 通过 ResolveBinding查询返回的引用记录。 有点那个左操作数的意味。
这就印证了引用记录章节的那句话
例如,赋值的左边操作数应该生成一个引用记录。

所以此时的lhs 应该如下
字段 | 值 |
---|---|
[[Base]] | 全局环境记录 |
[[ReferencedName]] | varA |
[[Strict]] | false |
[[ThisValue]] | 全局对象 |
- rhs
rhs 是 解析节点 StringLiteral 的执行结果,这里没有特别的操作,就是简单的返回值 "varA"。 有点那个右操作数的意味。
rref 这里传入的是 rhs, 执行流程如下,因为 rhs 是字符串值, 不是引用记录,直接就返回字符串值 即 "varA"。

如果是下面的代码, var varB = varA;
, 进行设置值的操作,,走到这一步进行 GetValue 时,varA
就是引用记录。
ini
javascript
复制代码
var varA = "varA";
var varB = varA;
这里 lref传入的是lhs, rval 是前面getValue执行的返回结果,本示例来说就是字符串 varA
;
此时的lref,即V 是引用记录,而且从上面得知 [[Base]]
是全局环境记录,所有走的流程是最后红色标记的 Else部分逻辑。
最终调用 全局环境记录 SetMutableBinding方法进行值的设置。


当 varA = 'varA'
这段代码走完,也是 这个节点解析完毕,全局环境环境记录上的 对象环境记录 上的
varA
才有了值。
constA = 'constA'
的执行流程是基本一致的,略过。
Block块内的代码执行逻辑也是一致的,只不过
var varA = 'block_varA';
的执行,会覆盖 varA的值let constA = 'block_constA';
constA
只会在块内生效(后续会详解讲解)
当代码执行到console.log(varA, constA);
时,关系图如下:

标识符绑定关系查找 和取值
console.log(varA, constA);
这行代码会输出字符串 block_varA 'constA'
。
第一步骤依旧是通过标志符 varA
和 constA
这里其实是两个引用记录,如果要想输出其背后相关联的值, 必然会存在一个取值过程。
取值的过程就很简单了
- 引用记录 GetValue
- 全局环境记录 GetBindingValue ( N, S )

最后varA
的查找

constA的查找

到此,应该基本清楚了
- 标志符绑定是怎么收集和创建的
- 标志符绑定怎么被赋值的
- 标志符绑定 环境记录 执行上下文的关系
- 标志符绑定 的查找 和取值
几个问题答案
- 什么是作用域
答:标识符绑定关系的可访问范围。协议标准关联最近的概念就是环境记录。
此答案仅供参考,不负任何责任,咻咻咻。 - 作用域是静态语义时就确定的, 还是运行时确定的
答:静态语义分析时就确定了。 各种申明绑定关系,申明实例化时创建的,var申明和函数申明在此阶段有初始化,var申明统一为undefined
, 而 函数申明则为函数对象。let/const/class的初始化都是运行时(代码执行时)行为。 初始化是通过评估Initializer
节点而得到的值。 - 下面的
varA
和varB
的查找有区别嘛
查找本质上没有大的区别,只是一个在全局环境记录中找到了,一个没找到。
javascript
javascript
复制代码
var varA = 'varA';
; (function fn1() {
"use strict"
(function fn2() {
"use strict"
(function fn3() {
"use strict"
console.log(varA);
console.log(varB);
})();
})();
})();
先看看最后的执行上下文和环境记录的关系图(放大看)
- 细心的同学会发现,函数环境记录的外层总有一个 申明环境记录,其作用其实就是保存函数名,会在 <<闭包 >> 的
具名函数表达式 和 匿名函数表达式
章节详细介绍。

varA 的查找流程, 先走的绿线,后走红线 (放大看)

varB 的查找也是一样的,只不过,没找到。
延伸
后申明的函数生效
javascript
javascript
复制代码
function varA(){console.log('varA_1')};
function varA(){console.log('varA_2')};
varA();
上面的代码输出 'varA_2'
, 是如何做到的。
GlobalDeclarationInstantiation全员顶层代码申明实例化时, 把函数申明倒序遍历,并且会按照 标识符名称 去重。

而普通的var 申明,还会单独再遍历一次

函数申明标志符绑定会初始化值
ini
javascript
复制代码
console.log(varA);
var varA = 'varA';
console.log(varA)
function varA(){};
上面的第一次会输出 函数,第二次会输出字符串 varA

因为在 GlobalDeclarationInstantiation全员顶层代码申明实例化时,函数申明已经设置过值了,下图中的fo
就是函数对象。

实际执行 var varA = 'varA';
, 只不过是更改了标识符绑定的值。
后申明的生效
ini
javascript
复制代码
var varA = 'varA';
var varA = 'varA2';
log(varA);
大家都知道上面的代码,最后 varA绑定的值 是 'varA2'
。这是怎么做到的呢?
- 申明实例化时 只创建了一个 varA 绑定
- 运行时 varA 值被 通过
Initializer
设置了两次,第二次生效了而已。
这里还是不要说赋值比较好,赋值在一些里面有专门的赋值语句 AssignmentExpression。虽然大致逻辑类似,但是毕竟还是两种行为。
后续
ini
javascript
复制代码
<script>
var varA = 'varA';
const constA = 'constA';
{
var varA = 'block_varA';
let constA = 'block_constA';
}
console.log(varA, constA);
</script>
回顾代码, 可能你已发现,为什么 块里面的代码一点都没提到,这是因为这是给下一节 作用域链用的,所以,走起。