解释 JavaScript Execution Context (执行上下文) 和 Lexical Environment (词法环境) 的创建过程和作用域链 (Scope Chain) 的构建。

JavaScript 执行上下文、词法环境和作用域链:一场代码的寻根之旅

大家好!我是你们今天的“代码寻宝向导”,我们将一起探索 JavaScript 这片神秘土地上的三个关键概念:执行上下文 (Execution Context)、词法环境 (Lexical Environment) 和作用域链 (Scope Chain)。 准备好你的探险装备(一杯咖啡和一颗好奇的心),让我们开始这场“代码寻根之旅”吧!

1. 什么是执行上下文? (Execution Context)

想象一下,JavaScript 代码就像一场舞台剧,而执行上下文就是这场剧的“后台”。 每个函数调用,或者整个脚本的执行,都会创建一个新的执行上下文。 这个上下文就像一个独立的“房间”,包含了代码运行所需的所有信息。

更具体地说,执行上下文是一个抽象的概念,它主要包含以下三个重要组成部分:

  • 变量环境 (Variable Environment): 存储变量和函数声明的地方。
  • 词法环境 (Lexical Environment): 存储变量和函数声明的地方,以及指向外部环境的引用。
  • this 绑定: 决定了 this 关键字指向的对象。

用一个不太严谨但方便理解的比喻,可以把执行上下文想象成一个“百宝箱”,里面装着:

百宝箱里的东西 对应执行上下文的组成部分 作用
变量和函数的“小纸条” 变量环境 & 词法环境 记录代码中声明的变量和函数,方便后续查找。
一张写着“我是谁”的纸条 this 绑定 告诉代码,this 关键字代表哪个对象。

JavaScript 引擎在执行代码之前,会创建一个全局执行上下文 (Global Execution Context)。 之后,每当调用一个函数,就会创建一个新的函数执行上下文 (Function Execution Context)。 当函数执行完毕,对应的执行上下文就会被销毁(当然,闭包情况除外,这个我们后面会讲到)。

2. 执行上下文的创建过程: 搭建舞台

执行上下文的创建分为两个阶段:

  1. 创建阶段 (Creation Phase): JavaScript 引擎会做一些准备工作,为代码的执行铺平道路。
  2. 执行阶段 (Execution Phase): 引擎开始逐行执行代码,利用创建阶段准备好的信息。

2.1 创建阶段: 准备“百宝箱”

在创建阶段,执行上下文会做以下几件事:

  • 创建词法环境 (Lexical Environment): 这是最重要的步骤之一,我们会在下一节详细讲解。
  • 创建变量环境 (Variable Environment): 初始时,变量环境和词法环境指向同一个对象。 在 ES6 引入 letconst 后,它们有所区别。
  • 确定 this 绑定: this 的值取决于函数的调用方式。 在全局执行上下文中,this 通常指向全局对象 (window 或 global)。
  • (可选)创建 arguments 对象: 对于函数执行上下文,如果函数有参数,会创建一个 arguments 对象,存储所有传入的参数。

2.2 执行阶段: 真正开始表演

在执行阶段,JavaScript 引擎会逐行执行代码,并执行以下操作:

  • 变量赋值: 将实际的值赋给变量。
  • 函数调用: 执行函数体内的代码,并创建新的函数执行上下文。
  • 表达式求值: 计算表达式的值。

3. 词法环境: 代码的“寻根地图”

词法环境 (Lexical Environment) 是一个非常关键的概念,它决定了变量和函数在代码中的可见性。 简单来说,词法环境就是一个“记录表”,记录了当前作用域内的变量和函数声明。

一个词法环境由两部分组成:

  • 环境记录 (Environment Record): 存储变量和函数声明的实际位置。
  • 外部环境引用 (Outer Environment Reference): 指向外部词法环境的引用。 这就是作用域链的基础!

我们可以把词法环境想象成一棵树,每个节点代表一个词法环境,节点之间通过“外部环境引用”连接起来。 这棵树的根节点就是全局词法环境。

3.1 词法环境的类型

词法环境主要有两种类型:

  • 全局词法环境 (Global Lexical Environment): 这是最外层的词法环境,它只有一个,对应全局执行上下文。 它的外部环境引用是 null。 在浏览器中,它包含了全局对象 (window) 和所有全局变量和函数。
  • 函数词法环境 (Function Lexical Environment): 每当调用一个函数,就会创建一个新的函数词法环境。 它的外部环境引用指向创建该函数的词法环境(记住是创建,不是调用!)。 这就是词法作用域的关键!

3.2 词法环境示例

let globalVar = "Global";

function outerFunction() {
  let outerVar = "Outer";

  function innerFunction() {
    let innerVar = "Inner";
    console.log(globalVar, outerVar, innerVar); // "Global" "Outer" "Inner"
  }

  innerFunction();
}

outerFunction();

在这个例子中,有三个词法环境:

  • 全局词法环境: 包含 globalVarouterFunction
  • outerFunction 的词法环境: 包含 outerVarinnerFunction,它的外部环境引用指向全局词法环境。
  • innerFunction 的词法环境: 包含 innerVar,它的外部环境引用指向 outerFunction 的词法环境。

4. 作用域链: 变量查找的“寻宝路线”

作用域链 (Scope Chain) 是 JavaScript 中查找变量的机制。 当代码需要访问一个变量时,JavaScript 引擎会按照作用域链的顺序,逐层查找变量,直到找到为止。

作用域链是由一系列词法环境组成的,它决定了代码在哪个“范围”内可以访问哪些变量。

4.1 作用域链的构建

作用域链是在执行上下文的创建阶段构建的。 具体来说,当创建一个函数执行上下文时,它的作用域链会包含以下内容:

  1. 当前函数的词法环境。
  2. 外部函数的词法环境(由外部环境引用决定)。
  3. 外部函数的外部函数的词法环境,以此类推,直到全局词法环境。

4.2 变量查找过程

当代码需要访问一个变量时,JavaScript 引擎会按照以下步骤查找变量:

  1. 在当前词法环境的“环境记录”中查找变量。
  2. 如果找到,就使用该变量。
  3. 如果没有找到,就沿着外部环境引用,到外部词法环境中查找。
  4. 重复步骤 2 和 3,直到找到变量或者到达全局词法环境。
  5. 如果在全局词法环境中仍然没有找到变量,就报错 (ReferenceError)(严格模式下) 或者隐式声明(非严格模式下)。

4.3 作用域链示例

让我们回到之前的例子:

let globalVar = "Global";

function outerFunction() {
  let outerVar = "Outer";

  function innerFunction() {
    let innerVar = "Inner";
    console.log(globalVar, outerVar, innerVar); // "Global" "Outer" "Inner"
  }

  innerFunction();
}

outerFunction();

innerFunction 中执行 console.log(globalVar, outerVar, innerVar) 时,JavaScript 引擎会按照以下顺序查找变量:

  1. innerVarinnerFunction 的词法环境中找到。
  2. outerVarinnerFunction 的词法环境中没有找到,于是沿着外部环境引用,到 outerFunction 的词法环境中查找,找到。
  3. globalVarinnerFunctionouterFunction 的词法环境中都没有找到,于是继续沿着外部环境引用,到全局词法环境中查找,找到。

5. 闭包: 作用域链的“时光机”

闭包 (Closure) 是 JavaScript 中一个非常强大且容易让人困惑的概念。 简单来说,闭包是指函数能够访问并记住其创建时的词法环境,即使该函数在其创建的词法环境之外执行。

换句话说,闭包就像一个“时光机”,让函数能够回到过去,访问以前的变量。

5.1 闭包的形成

当一个函数被创建时,它会记住其创建时的词法环境。 即使该函数在之后被传递到其他地方执行,它仍然能够访问并记住这个词法环境。

5.2 闭包示例

function outerFunction(outerVar) {
  function innerFunction() {
    console.log(outerVar);
  }
  return innerFunction;
}

let myClosure = outerFunction("Hello");
myClosure(); // "Hello"

在这个例子中,innerFunction 是一个闭包。 即使 outerFunction 已经执行完毕,并且其执行上下文已经被销毁,innerFunction 仍然能够访问 outerVar

这是因为 innerFunction 在创建时,记住了 outerFunction 的词法环境,并且通过作用域链,可以访问 outerVar

5.3 闭包的应用

闭包有很多应用,例如:

  • 创建私有变量: 通过闭包,可以将变量隐藏在函数内部,防止外部访问。
  • 保存状态: 闭包可以保存函数的状态,例如计数器。
  • 实现回调函数: 闭包可以确保回调函数能够访问正确的变量。

6. letconstvar 的区别

在 ES6 引入 letconst 之后,变量声明方式对执行上下文和词法环境产生了一些影响。

特性 var let const
作用域 函数作用域 块级作用域 块级作用域
提升 (Hoisting) 声明提升,值为 undefined 声明提升,但不能访问 (TDZ) 声明提升,但不能访问 (TDZ)
重复声明 允许在同一个作用域内重复声明 不允许在同一个作用域内重复声明 不允许在同一个作用域内重复声明
可变性 可以重新赋值 可以重新赋值 声明后不能重新赋值 (引用类型的值可以修改)

6.1 块级作用域

letconst 声明的变量具有块级作用域,这意味着它们只在声明它们的代码块 (例如 if 语句、for 循环或函数体) 内可见。

function example() {
  if (true) {
    let x = 10;
    const y = 20;
    var z = 30;
  }
  console.log(z); // 30
  console.log(x); // ReferenceError: x is not defined
  console.log(y); // ReferenceError: y is not defined
}

example();

6.2 暂时性死区 (Temporal Dead Zone, TDZ)

letconst 声明的变量会受到暂时性死区 (TDZ) 的影响。 这意味着在变量声明之前,即使变量已经存在于作用域中,也无法访问它。

console.log(x); // ReferenceError: Cannot access 'x' before initialization
let x = 10;

console.log(y); // ReferenceError: Cannot access 'y' before initialization
const y = 20;

6.3 变量环境和词法环境

var 声明的变量会被存储在变量环境中,而 letconst 声明的变量会被存储在词法环境中。 这使得 letconst 能够实现块级作用域,因为它们不会被提升到函数作用域的顶部。

7. 总结: 代码寻宝的收获

通过今天的“代码寻根之旅”,我们学习了 JavaScript 执行上下文、词法环境和作用域链的概念。 掌握这些概念对于理解 JavaScript 代码的执行方式至关重要。

记住,执行上下文是代码运行的“后台”,词法环境是代码的“寻根地图”,作用域链是变量查找的“寻宝路线”,而闭包则是作用域链的“时光机”。

希望今天的讲座能够帮助你更好地理解 JavaScript 这门语言。 如果你有任何问题,欢迎随时提问! 祝你在代码的世界里玩得开心!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注