词法环境——理解闭包背后的隐秘机制

https://amanexplains.com/lexical-envrionment-the%20hidden-part-to-understand-closures/

词法环境——理解闭包的隐藏拼图

  • 闭包
  • 词法
  • 作用域
  • JavaScript
当你刚踏入 JavaScript 的世界时,闭包可能是一个令人望而生畏的概念。在网上搜索一番,你会看到关于闭包是什么的无数种定义。但我感觉这些定义大多都很模糊,并没有解释清楚它们存在的根本原因。
今天,我们将尝试揭开这些概念的神秘面纱,这些概念都是 ECMAScript 262 规范的一部分,包括执行上下文词法环境标识符解析。此外,我们将了解到,正是因为这些机制,ECMAScript 中的所有函数实际上都是闭包。
我会先解释这些术语,然后给你展示一些代码示例,说明这些碎片是如何协同工作的。这将有助于巩固你的理解。

执行上下文

每当 JavaScript 解释器准备执行我们编写的函数或脚本时,它都会创建一个新的上下文。每一个脚本/代码都始于一个被称为全局执行上下文的执行上下文。
而每当我们调用一个函数时,一个新的执行上下文就会被创建并被放置到执行栈的顶部。当你调用一个嵌套函数,而该函数又调用了另一个嵌套函数时,同样的模式也会遵循:
image
让我们看看当我们的代码如上图所示执行时会发生什么:
  • 创建一个全局执行上下文,并将其放置在执行栈的底部。
  • 当调用 bar 时,创建一个新的 bar 执行上下文,并将其放在全局执行上下文的顶部。
  • 由于 bar 调用了嵌套函数 foo,一个新的 foo 执行上下文被创建,并被放置在 bar 执行上下文的顶部。
  • foo 返回时,它的上下文从栈中弹出,流程返回到 bar 上下文。
  • 一旦 bar 执行完毕,流程返回到全局上下文,最后,栈被清空。
执行栈的工作方式遵循后进先出的数据结构。它等待最顶部的执行上下文返回后,才执行下面的上下文。
从概念上讲,执行上下文的结构如下所示:
// Execution context in ES5
ExecutionContext = {
  ThisBinding: <this value>,
  VariableEnvironment: { ... },
  LexicalEnvironment: { ... }
}

 

别担心,如果这个结构看起来很吓人。我们很快就会逐一查看这些组件。需要记住的关键点是,每次调用执行上下文都有两个阶段:创建阶段执行阶段。创建阶段是指上下文被创建但尚未调用的时刻。
在创建阶段会发生以下几件事:
  • 变量环境组件用于变量、参数和函数声明的初始存储。用 var 声明的变量会被初始化为 undefined
  • This 的值被确定。
  • 词法环境在此阶段只是变量环境的一个副本。
进入执行阶段后:
  • 值被赋给变量。
  • 词法环境用于解析绑定。
现在,让我们尝试理解一下什么是词法环境。

词法环境

根据 ECMAScript 规范 262 (8.1):
词法环境是一种规范类型,用于根据 ECMAScript 代码的词法嵌套结构,来定义标识符与特定变量和函数之间的关联。
让我们尝试把这里的一些概念简单化。一个词法环境由两个主要组件构成:环境记录和指向外部(父级)词法环境的引用:
var x = 10;

function foo(){
  var y = 20;
 console.log(x+y); // 30
}

// Environment technically consist of two main components:
// environmentRecord, and a reference to the outer environment

// Environment of the global context
globalEnvironment = {
  environmentRecord: {
    // built-ins
    // our bindings:
    // highlight-next-line
    x: 10
  },
  outer: null // no parent environment
};

// Environment of the "foo" function
fooEnvironment = {
  environmentRecord: {
    y: 20
  },
  // highlight-next-line
  outer: globalEnvironment
};
从视觉上看,它会是这样的:
image
正如你所见,当试图在 foo 上下文中解析标识符 “y” 时,系统会去访问外部环境(全局环境)。这个过程被称为标识符解析,它发生在执行上下文的运行阶段。
现在,既然我们已经掌握了关于环境的这些知识,让我们回到执行上下文的结构,看看那里到底发生了什么:
变量环境:它的环境记录用于变量、参数和函数声明的初始存储,随后在进入上下文激活阶段时会被填充具体的值。
function foo(a) {
  var b = 20;
}
foo(10);

// The VariableEnvironment component of the foo function
//context at creation stage
fooContext.VariableEnvironment = {
  environmentRecord: {
    arguments: { 0: 10, length: 1, callee: foo },
    a: 10,
    b: undefined
  },
  outer: globalEnvironment
};

// After the execution stage, the VE envRec
// table is filled in with the value
fooContext.VariableEnvironment = {
  environmentRecord: {
    arguments: { 0: 10, length: 1, callee: foo },
    a: 10,
    b: 20
  },
  outer: globalEnvironment
};
 
  • 词法环境:最初,它只是变量环境的一个副本。在上下文运行阶段,它用于确定出现在上下文中的标识符的绑定。
变量环境和词法环境本质上都是词法环境,也就是说,它们都会静态地(在创建阶段)捕获上下文中创建的内部函数的外部绑定。正是这种机制催生了闭包
    • 静态地捕获内部函数的外部绑定,从而导致了闭包的形成。
    • 标识符解析,也就是作用域链查找

      在理解闭包之前,让我们先了解一下作用域链是如何在我们的执行上下文中创建的。正如我们之前看到的,每个执行上下文都有一个用于标识符解析的词法环境。上下文的所有本地绑定都存储在环境记录表中。
      如果在当前的环境记录中无法解析标识符,解析过程就会继续转向外部(父级)环境记录表。这种模式会一直持续,直到标识符被解析为止。如果找不到,就会抛出一个 ReferenceError(引用错误)。
      这与原型查找链非常相似。现在,这里需要记住的关键点是:词法环境会在上下文创建阶段静态地(按词法)捕获外部绑定,并在运行上下文(执行阶段)中原样使用它。

闭包

正如我们在上一节所看到的,在函数创建阶段,内部上下文的词法环境中静态保存外部绑定的行为,就催生了闭包——无论该函数之后是否会被激活。让我们通过一个例子来看看这一点:
var a = 10;
function foo(){
  console.log(a);
};
function bar(){
  var a = 20;
  foo();
};
bar(); // will print "10"
foo 的词法环境在创建时就捕获了绑定 “a”,当时的值是 10。所以,当 foo 稍后被调用时(在执行阶段),标识符 “a” 解析出来的值是 10,而不是 20。
从概念上讲,这个标识符解析过程看起来会是这样的:
// check for binding "a" in the env record of "foo"
-- foo.[[LexicalEnvironment]].[[Record]] --> not found

// if not found, check for its outer environment
// highlight-next-line
--- global[[LexicalEnvironment]][[Record]] --> found 10
// resolve the identifier with a value of 1
image
示例二 
 
 
function outer() {
 let id = 1;

 // creating a function would statically captures
 // the value of 'id' here
 return function inner(){
  console.log(id);
  }
};

const innerFunc = outer();
innerFunc(); // prints 1;
当外部函数返回时,它的执行上下文会从执行栈中弹出。但是,当我们稍后调用 innerFunc() 时,它仍然能够打印出正确的值,这是因为内部函数的词法环境在创建时,就已经静态地捕获了其外部(父级)环境的 “id” 绑定。 
 
// check for binding "id" in the env record of "inner"
-- inner.[[LexicalEnvironment]].[[Record]] --> not found
// if not found, check for its outer environment (outer)
// highlight-next-line
--- outer[[LexicalEnvironment]][[Record]] --> found 1
// resolve the identifier with a value of 1
image 

结论

  • 执行上下文栈遵循后进先出(LIFO)的数据结构。
  • 只有一个全局上下文,我们的代码或脚本就是在这里执行的。
  • 调用函数会创建一个新的执行上下文。如果它包含嵌套函数调用,则会创建一个新上下文并将其置于父上下文之上。当函数执行完毕后,它会从栈中弹出,流程返回到栈中下方的上下文。
  • 词法环境有两个主要组件:环境记录和对外部环境的引用。
  • 变量环境词法环境都会静态地捕获上下文中创建的内部函数的外部绑定。所有函数在创建阶段都会静态地(按词法)捕获其父环境的外部绑定。这使得嵌套函数即使在其父上下文已从执行栈中清除后,仍然可以访问外部绑定。这种机制正是 JavaScript 中闭包的基础。
希望这篇文章读起来很有趣,也不会让你感到不知所措。如果你想进一步讨论,欢迎给我发推文。祝编码愉快 😊。
 
 
posted @ 2026-04-06 17:18  chenlight  阅读(2)  评论(0)    收藏  举报