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. 执行上下文的创建过程: 搭建舞台
执行上下文的创建分为两个阶段:
- 创建阶段 (Creation Phase): JavaScript 引擎会做一些准备工作,为代码的执行铺平道路。
- 执行阶段 (Execution Phase): 引擎开始逐行执行代码,利用创建阶段准备好的信息。
2.1 创建阶段: 准备“百宝箱”
在创建阶段,执行上下文会做以下几件事:
- 创建词法环境 (Lexical Environment): 这是最重要的步骤之一,我们会在下一节详细讲解。
- 创建变量环境 (Variable Environment): 初始时,变量环境和词法环境指向同一个对象。 在 ES6 引入
let
和const
后,它们有所区别。 - 确定
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();
在这个例子中,有三个词法环境:
- 全局词法环境: 包含
globalVar
和outerFunction
。 outerFunction
的词法环境: 包含outerVar
和innerFunction
,它的外部环境引用指向全局词法环境。innerFunction
的词法环境: 包含innerVar
,它的外部环境引用指向outerFunction
的词法环境。
4. 作用域链: 变量查找的“寻宝路线”
作用域链 (Scope Chain) 是 JavaScript 中查找变量的机制。 当代码需要访问一个变量时,JavaScript 引擎会按照作用域链的顺序,逐层查找变量,直到找到为止。
作用域链是由一系列词法环境组成的,它决定了代码在哪个“范围”内可以访问哪些变量。
4.1 作用域链的构建
作用域链是在执行上下文的创建阶段构建的。 具体来说,当创建一个函数执行上下文时,它的作用域链会包含以下内容:
- 当前函数的词法环境。
- 外部函数的词法环境(由外部环境引用决定)。
- 外部函数的外部函数的词法环境,以此类推,直到全局词法环境。
4.2 变量查找过程
当代码需要访问一个变量时,JavaScript 引擎会按照以下步骤查找变量:
- 在当前词法环境的“环境记录”中查找变量。
- 如果找到,就使用该变量。
- 如果没有找到,就沿着外部环境引用,到外部词法环境中查找。
- 重复步骤 2 和 3,直到找到变量或者到达全局词法环境。
- 如果在全局词法环境中仍然没有找到变量,就报错 (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 引擎会按照以下顺序查找变量:
innerVar
: 在innerFunction
的词法环境中找到。outerVar
: 在innerFunction
的词法环境中没有找到,于是沿着外部环境引用,到outerFunction
的词法环境中查找,找到。globalVar
: 在innerFunction
和outerFunction
的词法环境中都没有找到,于是继续沿着外部环境引用,到全局词法环境中查找,找到。
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. let
、const
和 var
的区别
在 ES6 引入 let
和 const
之后,变量声明方式对执行上下文和词法环境产生了一些影响。
特性 | var |
let |
const |
---|---|---|---|
作用域 | 函数作用域 | 块级作用域 | 块级作用域 |
提升 (Hoisting) | 声明提升,值为 undefined |
声明提升,但不能访问 (TDZ) | 声明提升,但不能访问 (TDZ) |
重复声明 | 允许在同一个作用域内重复声明 | 不允许在同一个作用域内重复声明 | 不允许在同一个作用域内重复声明 |
可变性 | 可以重新赋值 | 可以重新赋值 | 声明后不能重新赋值 (引用类型的值可以修改) |
6.1 块级作用域
let
和 const
声明的变量具有块级作用域,这意味着它们只在声明它们的代码块 (例如 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)
let
和 const
声明的变量会受到暂时性死区 (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
声明的变量会被存储在变量环境中,而 let
和 const
声明的变量会被存储在词法环境中。 这使得 let
和 const
能够实现块级作用域,因为它们不会被提升到函数作用域的顶部。
7. 总结: 代码寻宝的收获
通过今天的“代码寻根之旅”,我们学习了 JavaScript 执行上下文、词法环境和作用域链的概念。 掌握这些概念对于理解 JavaScript 代码的执行方式至关重要。
记住,执行上下文是代码运行的“后台”,词法环境是代码的“寻根地图”,作用域链是变量查找的“寻宝路线”,而闭包则是作用域链的“时光机”。
希望今天的讲座能够帮助你更好地理解 JavaScript 这门语言。 如果你有任何问题,欢迎随时提问! 祝你在代码的世界里玩得开心!