前言
ECMAScript标准是深入学习JavaScript原理最好的资料,没有其二。
通过增加对ECMAScript语言的理解,理解javascript现象后面的逻辑,提升个人编码能力。
欢迎关注和订阅专栏 重学前端-ECMAScript协议上篇
先来几个问题
-
什么是作用域
-
作用域是静态语义分析时就确定的, 还是运行时(代码执行时)确定的
-
下面的
varA和varB的查找有区别嘛javascript 复制代码 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的申明语句 |
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
复制代码
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"]。
场景一: 顶层代码
javascript
复制代码
<script>
"use strict"
var varA = "varA";
const constA = "constA";
let letA = "letA";
function funcA(){};
class ClassA {};
</script>
执行上下文,环境记录和标志符标定关系如下:
场景二: 代码块
在块内执行 VarDeclaredNames 的结果 :["varA"],此时的函数申明 funcA 属于词法申明队列了。
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)
两种操作,创建表示有这个标识符的名称了,但是没有绑定值, 从代码层面理解,可以理解为
- 实例化 = 申明
- 初始化 = 设置值
下面从一段简单的全局顶层代码,一起来理解标识符绑定的创建,实例化,以及查找。
全局顶层代码示例
示例代码如下:
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]] 大致的内容如下:
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 ) 会对整个解析树进行静态分析,提取标识符名和创建标识符绑定关系。 GlobalDeclarationInstantiation ( script, env ) 的整个流程在 《script加载和全局顶层代码申明实例化》章节有详细过程。
标识符创建和绑定是在静态语义分析的时候处理的,还没有到真正的执行代码。
申明分为两类:
- 变量申明 VarDeclaredNames: var 申明,顶层代码的函数申明
- 词法申明 LexicallyDeclaredNames:let/const/ class 申明,非顶层的函数申明
VarDeclaredNames
获取变量申明的过程,你可以理解为就是检查节点上的申明和语句,匹配特定节点类型
- VariableStatement
- VariableDeclaration
- ExportDeclaration
- CaseBlock
- 等等
然后按照规律去获取对应节点标志符的名称。本示例就是找 BindingIdentifier节点 name 的值。
本示例的返回值是 两个 [ varA , varA ]。
大致的逻辑用代码表示如下:
而本示例属于 传入的node 就是 type 为 Script的根节点。
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类型的节点。
用代码表示大致如下:
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 ) ,整个流程在 《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
复制代码
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 这两个东西,到底是什么?
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 都有的逻辑
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就是引用记录。
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
复制代码
var varA = 'varA';
; (function fn1() {
"use strict"
(function fn2() {
"use strict"
(function fn3() {
"use strict"
console.log(varA);
console.log(varB);
})();
})();
})();
先看看最后的执行上下文和环境记录的关系图(放大看)
- 细心的同学会发现,函数环境记录的外层总有一个 申明环境记录,其作用其实就是保存函数名,会在 <<闭包>> 的
具名函数表达式 和 匿名函数表达式章节详细介绍。
varA 的查找流程, 先走的绿线,后走红线 (放大看)
varB 的查找也是一样的,只不过,没找到。
延伸
后申明的函数生效
javascript
复制代码
function varA(){console.log('varA_1')};
function varA(){console.log('varA_2')};
varA();
上面的代码输出 'varA_2', 是如何做到的。
GlobalDeclarationInstantiation 全员顶层代码申明实例化时, 把函数申明倒序遍历,并且会按照 标识符名称 去重。
而普通的var 申明,还会单独再遍历一次
函数申明标志符绑定会初始化值
javascript
复制代码
console.log(varA);
var varA = 'varA';
console.log(varA)
function varA(){};
上面的第一次会输出 函数,第二次会输出字符串 varA
因为在 GlobalDeclarationInstantiation 全员顶层代码申明实例化时,函数申明已经设置过值了,下图中的fo就是函数对象。
实际执行 var varA = 'varA';, 只不过是更改了标识符绑定的值。
后申明的生效
javascript
复制代码
var varA = 'varA';
var varA = 'varA2';
log(varA);
大家都知道上面的代码,最后 varA绑定的值 是 'varA2'。这是怎么做到的呢?
- 申明实例化时 只创建了一个 varA 绑定
- 运行时 varA 值被 通过
Initializer设置了两次,第二次生效了而已。
这里还是不要说赋值比较好,赋值在一些里面有专门的赋值语句 AssignmentExpression。虽然大致逻辑类似,但是毕竟还是两种行为。
后续
javascript
复制代码
<script>
var varA = 'varA';
const constA = 'constA';
{
var varA = 'block_varA';
let constA = 'block_constA';
}
console.log(varA, constA);
</script>
回顾代码, 可能你已发现,为什么 块里面的代码一点都没提到,这是因为这是给下一节 作用域链用的,所以,走起。