ECMAScript 标识符绑定关系和作用域

157 阅读17分钟

前言

ECMAScript标准是深入学习JavaScript原理最好的资料,没有其二。

通过增加对ECMAScript语言的理解,理解javascript现象后面的逻辑,提升个人编码能力。

欢迎关注和订阅专栏 重学前端-ECMAScript协议上篇

先来几个问题

  1. 什么是作用域

  2. 作用域是静态语义分析时就确定的, 还是运行时(代码执行时)确定的

  3. 下面的 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
ClassDeclarationclass的申明
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顶层代码和函数顶层代码)

两种情况下,函数申明是有区别的,记住后面要考的:

词法申明名和变量申明名 与 四种申明 的关系如下

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>

代码执行的基本流程

整个代码的基本执行流程是(忽略亿点点细节)

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 : [varAvarA]
  • LexicallyDeclaredNames: [constA]

虽然这里 变量申明有两个 varA,但是在申明实例化的第9和第10步,会去重,所以最后只会创建一个绑定关系。(varDeclarations 是 和 VarDeclaredNames 对应的,只不过是解析节点)

GlobalDeclarationInstantiation ( script, env ) 中的 env 参数就是全局环境记录,而整个全局顶层代码申明实例化过程,所有的词法申明和变量申明的绑定关系都是在 env上创建的。 相比函数申明实例化,是简单太多了。

全局环境记录又是由 对象环境记录 和 申明环境记录组成的, 词法申明和变量申明对应的绑定关系如下。

VarDeclaredNamesLexicallyDeclaredNames
创建方法env.CreateGlobalVarBinding (var)env.CreateGlobalFunctionBinding (function)env.CreateMutableBinding (let/class)env.CreateImmutableBinding (const)
对象环境记录
申明环境记录

说明:

  • 变量申明:var 申明 和 函数申明的绑定关系虽然都在 对象环境记录上,但又是不一样的。

下面的 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);时,关系图如下:

image.png

标识符绑定关系查找 和取值

console.log(varA, constA); 这行代码会输出字符串 block_varA 'constA' 

第一步骤依旧是通过标志符 varA和 constA这里其实是两个引用记录,如果要想输出其背后相关联的值, 必然会存在一个取值过程。

取值的过程就很简单了

最后varA的查找

image.png

constA的查找

image.png

到此,应该基本清楚了

  • 标志符绑定是怎么收集和创建的
  • 标志符绑定怎么被赋值的
  • 标志符绑定 环境记录 执行上下文的关系
  • 标志符绑定 的查找 和取值

几个问题答案

  1. 什么是作用域
    答:标识符绑定关系的可访问范围。协议标准关联最近的概念就是环境记录。
    此答案仅供参考,不负任何责任,咻咻咻。
  2. 作用域是静态语义时就确定的, 还是运行时确定的
    答:静态语义分析时就确定了。 各种申明绑定关系,申明实例化时创建的,var申明和函数申明在此阶段有初始化,var申明统一为 undefined, 而 函数申明则为函数对象。let/const/class的初始化都是运行时(代码执行时)行为。 初始化是通过评估 Initializer节点而得到的值。
  3. 下面的 varA和 varB的查找有区别嘛

查找本质上没有大的区别,只是一个在全局环境记录中找到了,一个没找到。

javascript
复制代码
var varA = 'varA';

; (function fn1() {
    "use strict"
    (function fn2() {
        "use strict"
        (function fn3() {
            "use strict"
            console.log(varA);
            console.log(varB);

        })();
    })();
})();

先看看最后的执行上下文和环境记录的关系图(放大看)

  • 细心的同学会发现,函数环境记录的外层总有一个 申明环境记录,其作用其实就是保存函数名,会在 <<闭包>> 的 具名函数表达式 和 匿名函数表达式 章节详细介绍。

image.png

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

image.png

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>

回顾代码, 可能你已发现,为什么 块里面的代码一点都没提到,这是因为这是给下一节 作用域链用的,所以,走起。