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

前言

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

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

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

先来几个问题

  1. 什么是作用域

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

  3. 下面的 varAvarB的查找有区别嘛

    ini 复制代码
    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的申明语句
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顶层代码和函数顶层代码)

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

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

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>

代码执行的基本流程

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

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`节点,对应的就是标志符的名称对应的节点。 ![](https://oss.xyyzone.com/jishuzhan/article/1915327238347751425/17e4a210751a4b7c64ade34882920237.webp) ### 解析变量申明和词法申明 [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 申明 和 函数申明的绑定关系虽然都在 对象环境记录上,但又是不一样的。

下面的 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就是从执行上下文的词法环境记录开始找

到此,全局顶层代码的申明实例化完毕,varAconstA 都只是申明绑定关系,并没有设置值的。

代码执行

先提个问题:

当示例代码,执行到 console.log(varA, constA);这句代码时, 请问 varAconstA 这两个东西,到底是什么?

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' 走的是 BindingIdentifierInitializer 都有的逻辑

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'

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

取值的过程就很简单了

最后varA的查找

constA的查找

到此,应该基本清楚了

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

几个问题答案

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

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

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>

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

相关推荐
layman052812 分钟前
node.js 实战——(fs模块 知识点学习)
javascript·node.js
毕小宝13 分钟前
编写一个网页版的音频播放器,AI 加持,So easy!
前端·javascript
万水千山走遍TML13 分钟前
JavaScript性能优化
开发语言·前端·javascript·性能优化·js·js性能
Aphasia31114 分钟前
react必备JS知识点(一)——判断this指向👆🏻
前端·javascript·react.js
会飞的鱼先生29 分钟前
vue3中slot(插槽)的详细使用
前端·javascript·vue.js
小小小小宇44 分钟前
一文搞定CSS Grid布局
前端
0xHashlet1 小时前
Dapp实战案例002:从零部署链上计数器合约并实现前端交互
前端
知心宝贝1 小时前
🔍 从简单到复杂:JavaScript 事件处理的全方位解读
前端·javascript·面试
安余生大大1 小时前
关于Safari浏览器在ios<16.3版本不支持正则表达式零宽断言的解决办法
前端
前端涂涂1 小时前
express查看文件上传报文,处理文件上传,以及formidable包的使用
前端·后端