各位同学,晚上好! 今天咱们聊聊 JavaScript 这门语言中一个非常核心,但经常被忽视的概念:执行上下文(Execution Context)和词法环境(Lexical Environment),以及由它们构建的作用域链(Scope Chain)。 这玩意儿听起来有点吓人,但实际上理解了之后,你会发现 JavaScript 的很多行为变得理所当然了。 别害怕,咱们用大白话,结合代码例子,一层一层地扒开它的皮。
首先,打个招呼。 你可以叫我老司机,今天就带大家飙车,好好认识一下JavaScript的 “内幕”。
一、什么是执行上下文? 想象一下一个剧场
把 JavaScript 想象成一个剧场。 剧场里有很多戏要演,每部戏就是一个函数。 但剧场不可能同时演好几部戏,一次只能演一部。 那么,"执行上下文" 就是这出戏的舞台!
执行上下文是一个抽象的概念,它是 JavaScript 代码被执行时创建的一个环境。 这个环境包含了代码执行所需的所有信息:变量、函数、this 的指向等等。
可以把执行上下文看作是一个 JavaScript 代码片段(通常是一个函数)运行的“沙箱”。 这个沙箱有自己的变量、函数和权限,不会干扰其他沙箱的运行。
类型:
JavaScript 中有三种类型的执行上下文:
- 全局执行上下文(Global Execution Context): 这是最外层的上下文,也是默认的上下文。 在浏览器中,它通常对应
window
对象;在 Node.js 中,它对应global
对象。 全局代码(不在任何函数内部的代码)就在这个上下文中执行。 - 函数执行上下文(Function Execution Context): 每当调用一个函数时,就会创建一个新的函数执行上下文。 这个上下文包含了函数内部的变量、参数和
this
的指向。 - Eval 执行上下文(Eval Execution Context): 当使用
eval()
函数时,会创建一个新的执行上下文。 (不建议使用eval()
,因为它存在安全风险和性能问题。)
执行上下文的生命周期
执行上下文的生命周期包括三个阶段:
-
创建阶段(Creation Phase): 这是执行上下文被创建的阶段,也是最重要的阶段。 在这个阶段,JavaScript 引擎会做以下几件事情:
- 创建词法环境(Lexical Environment): 词法环境是存储变量和函数声明的地方。
- 创建变量环境(Variable Environment): 变量环境也是存储变量的地方,但它只存储
var
声明的变量和函数声明。 - 确定
this
的指向:this
的指向取决于函数的调用方式。 - 创建作用域链(Scope Chain): 作用域链用于查找变量。
-
执行阶段(Execution Phase): 在这个阶段,JavaScript 引擎会执行代码,并为变量赋值。
-
销毁阶段(Teardown Phase): 当代码执行完毕后,执行上下文会被销毁。
二、词法环境(Lexical Environment): 变量的家
词法环境是执行上下文的一个重要组成部分。 简单来说,它就是一个存储变量和函数声明的数据结构。 它有点像一个 JavaScript 对象的集合, 存储了变量和函数之间的对应关系。
词法环境由两个部分组成:
- 环境记录(Environment Record): 存储变量和函数声明的实际位置。
- 声明式环境记录(Declarative Environment Record): 用于存储函数声明和变量声明(
let
,const
,class
)。 - 对象式环境记录(Object Environment Record): 用于存储全局变量(在全局执行上下文中)。
- 声明式环境记录(Declarative Environment Record): 用于存储函数声明和变量声明(
- 外部词法环境引用(Outer Lexical Environment): 指向外部(父级)词法环境的指针。 这个指针是作用域链的关键。
让我们看一个例子:
let globalVar = "我是全局变量";
function outerFunction() {
let outerVar = "我是外部函数变量";
function innerFunction() {
let innerVar = "我是内部函数变量";
console.log(globalVar, outerVar, innerVar);
}
innerFunction();
}
outerFunction(); // 输出: 我是全局变量 我是外部函数变量 我是内部函数变量
在这个例子中,有三个词法环境:
- 全局词法环境: 包含
globalVar
和outerFunction
。 outerFunction
的词法环境: 包含outerVar
和innerFunction
。 它的外部词法环境引用指向全局词法环境。innerFunction
的词法环境: 包含innerVar
。 它的外部词法环境引用指向outerFunction
的词法环境。
三、作用域链(Scope Chain): 寻宝图
现在,我们来聊聊作用域链。 作用域链就像一张寻宝图,它告诉 JavaScript 引擎在哪里查找变量。
当 JavaScript 引擎试图访问一个变量时,它会按照以下步骤进行查找:
- 首先,在当前词法环境中查找。 如果找到了,就使用这个变量。
- 如果没有找到,就沿着外部词法环境引用向上查找,直到找到全局词法环境。
- 如果在全局词法环境中仍然没有找到,就报错(
ReferenceError
)。
所以,作用域链是由当前执行上下文的词法环境以及所有外部词法环境组成的链式结构。
回到之前的例子:
let globalVar = "我是全局变量";
function outerFunction() {
let outerVar = "我是外部函数变量";
function innerFunction() {
let innerVar = "我是内部函数变量";
console.log(globalVar, outerVar, innerVar);
}
innerFunction();
}
outerFunction();
当 innerFunction
内部的 console.log
尝试访问 globalVar
时,它会:
- 在
innerFunction
的词法环境中查找: 没有找到globalVar
。 - 沿着外部词法环境引用,在
outerFunction
的词法环境中查找: 仍然没有找到globalVar
。 - 沿着外部词法环境引用,在全局词法环境中查找: 找到了
globalVar
,于是使用它。
这就是作用域链的作用。 它允许内部函数访问外部函数的变量,以及全局变量。
四、变量提升(Hoisting): 神奇的魔法
在 JavaScript 中,var
声明的变量和函数声明会被“提升”到其作用域的顶部。 这意味着你可以在声明之前使用它们,而不会报错。
console.log(myVar); // 输出: undefined
var myVar = "Hello";
myFunction(); // 输出: Hello from myFunction
function myFunction() {
console.log("Hello from myFunction");
}
注意: 只有声明会被提升,赋值不会被提升。 所以,myVar
在声明之前的值是 undefined
。
let
和 const
声明的变量也会被提升,但它们不会被初始化。 这意味着你不能在声明之前使用它们,否则会报错(ReferenceError
)。 这种状态被称为“暂时性死区”(Temporal Dead Zone,TDZ)。
console.log(myLet); // 报错: ReferenceError: Cannot access 'myLet' before initialization
let myLet = "Hello";
五、闭包(Closure): 永恒的记忆
闭包是 JavaScript 中一个非常强大,但也容易让人困惑的概念。 简单来说,闭包是指函数可以访问并记住其词法环境,即使在其词法环境已经不存在的情况下。
换句话说,一个函数可以“封闭”住它创建时所处的词法环境,并一直持有对该环境的访问权。
function createCounter() {
let count = 0;
return function() {
count++;
console.log(count);
};
}
const counter1 = createCounter();
counter1(); // 输出: 1
counter1(); // 输出: 2
const counter2 = createCounter();
counter2(); // 输出: 1
在这个例子中,createCounter
函数返回了一个内部函数。 这个内部函数可以访问 createCounter
函数的变量 count
,即使 createCounter
函数已经执行完毕。
这就是闭包。 内部函数“封闭”住了 createCounter
函数的词法环境,并一直持有对 count
变量的访问权。
闭包的用途
闭包有很多用途,包括:
- 创建私有变量: 通过闭包,可以创建只能在函数内部访问的变量。
- 实现柯里化(Currying): 柯里化是指将一个接受多个参数的函数转换为一系列接受单个参数的函数。
- 创建事件处理器: 闭包可以用于在事件处理器中访问外部变量。
六、 this
关键字: 迷之指向
this
关键字是 JavaScript 中另一个让人困惑的概念。 this
的指向取决于函数的调用方式。
- 默认绑定: 在非严格模式下,
this
指向全局对象(浏览器中是window
,Node.js 中是global
)。 在严格模式下,this
指向undefined
。 - 隐式绑定: 当函数作为对象的方法调用时,
this
指向该对象。 - 显式绑定: 可以使用
call()
,apply()
,bind()
方法显式地指定this
的指向。 new
绑定: 当使用new
关键字调用函数时,this
指向新创建的对象。- 箭头函数: 箭头函数不绑定
this
,它会继承外层作用域的this
。
七、执行上下文,词法环境, 作用域链结合的例子。
var a = 10; // 全局变量
function outer() {
var b = 20; // outer 函数的局部变量
function inner() {
var c = 30; // inner 函数的局部变量
console.log(a + b + c); // 访问 a, b, c
}
inner(); // 调用 inner 函数
}
outer(); // 调用 outer 函数
执行过程和上下文分析
-
全局执行上下文 (GEC):
- 创建:
- 词法环境:
{ a: undefined, outer: <function> }
- 变量环境:
{ a: 10, outer: <function> }
(此时a
被赋值) this
: 指向全局对象 (window 或 global)
- 词法环境:
- 执行:
a = 10
- 定义
outer
函数 - 调用
outer()
- 创建:
-
outer
函数执行上下文 (OEC):- 创建:
- 词法环境:
{ b: undefined, inner: <function> }
- 变量环境:
{ b: 20, inner: <function> }
(此时b
被赋值) - 外部词法环境引用: 指向 GEC 的词法环境
this
: 指向全局对象 (因为是默认绑定)
- 词法环境:
- 执行:
b = 20
- 定义
inner
函数 - 调用
inner()
- 创建:
-
inner
函数执行上下文 (IEC):- 创建:
- 词法环境:
{ c: undefined }
- 变量环境:
{ c: 30 }
(此时c
被赋值) - 外部词法环境引用: 指向 OEC 的词法环境
this
: 指向全局对象 (因为是默认绑定)
- 词法环境:
- 执行:
c = 30
console.log(a + b + c)
:- 查找
a
: 在 IEC 中找不到,沿着作用域链到 OEC 中找不到,再到 GEC 中找到a = 10
- 查找
b
: 在 IEC 中找不到,沿着作用域链到 OEC 中找到b = 20
- 查找
c
: 在 IEC 中找到c = 30
- 计算
10 + 20 + 30 = 60
- 输出
60
- 查找
- 创建:
总结
JavaScript 的执行上下文、词法环境和作用域链是理解 JavaScript 代码如何运行的关键。 它们决定了变量的可见性和生命周期,以及 this
的指向。 理解了这些概念,你就可以更好地理解 JavaScript 的行为,并编写更健壮、更可维护的代码。
八、一些补充说明:
概念 | 描述 | 关键点 |
---|---|---|
执行上下文 | 代码执行的环境,包括变量、函数、this 等信息。 |
分为全局、函数、eval 执行上下文; 有创建、执行、销毁阶段。 |
词法环境 | 存储变量和函数声明的数据结构。 | 由环境记录和外部词法环境引用组成; 作用域链的基础。 |
作用域链 | 查找变量的链式结构,由当前执行上下文的词法环境以及所有外部词法环境组成。 | 决定了变量的可见性。 |
变量提升 | var 声明的变量和函数声明会被提升到作用域顶部。 |
let 和 const 也有提升,但存在暂时性死区。 |
闭包 | 函数可以访问并记住其词法环境,即使在其词法环境已经不存在的情况下。 | 可以创建私有变量。 |
this 关键字 |
指向取决于函数的调用方式。 | 默认绑定、隐式绑定、显式绑定、new 绑定、箭头函数。 |
九、总结陈词
好了,各位同学,今天的课就上到这里。 希望通过今天的讲解,大家对 JavaScript 的执行上下文、词法环境和作用域链有了更深入的理解。 这些概念可能有点抽象,需要多加练习和思考。 记住,理解了这些底层原理,你才能真正掌握 JavaScript 这门语言。
以后写代码的时候,脑子里多想想这些,保证你写出来的代码更加清晰明了,bug 也少!
下课! 别忘了课后复习,多敲代码,熟能生巧!