JS — 深入理解 JavaScript 中的词法环境 — 深度剖析 — 第一部分
作为一名开发者,我经常遇到“词法环境”这个术语,但我从未真正花时间去深入探究它。所以,我决定深挖一番,并在这篇文章中记录我的发现——毕竟“分享就是关爱嘛 ;)”。
读完这篇文章后,希望我们都能对什么是词法环境有一个扎实的理解。我们还将一起探索内存中发生了什么、什么是数据结构,以及调用栈是如何工作的。别担心——我会尽量讲得简单明了!
词法环境
在深入细节之前,让我先做一个简要的概述。如果有些概念乍看之下很复杂,请别担心——我会把它们拆解开来,并用类比的方式让它们更容易理解。
词法环境是 JavaScript 中一种特殊的数据结构,它负责追踪代码在特定位置时的变量和函数作用域。
数据结构是计算机组织和存储信息的方式,以便高效地使用它们。常见的例子包括数组、对象、列表和树。查看更多:数据结构教程 - GeeksforGeeks
“词法”这个词意味着变量和函数的作用域及可访问性是由它们在代码中书写的位置决定的,而不是由程序运行的方式决定的。
词法环境的关键作用:
- 它存储特定作用域(如一个函数或一个代码块)内的所有变量和函数声明。
- 它让你可以在代码的特定位置访问这些存储的内容。
下面是一个简单的代码示例,其中包含了三个不同的词法环境:
var sheep = 1; // Global sheep
function drinkWater() {
let waterTemperature = "cold";
console.log("The sheep is drinking " + waterTemperature + " water.");
}
var sheepHouseInArea = true; // Indicates whether the sheep house is present
if (sheepHouseInArea) {
let lambs = 2; // Lambs inside the house
console.log("There are " + sheep + " sheep in the total area and "
+ lambs + " lambs!");
}
// This will result in an error because 'lambs'
// is only accessible inside the if block!
console.log("How many lambs are there? " + lambs);
这段代码里的三个词法环境分别是:全局作用域、
drinkWater 函数作用域,以及 if 代码块作用域。为了让这些概念更易于理解,我们用关于绵羊的简单类比来说明:绵羊类比
这周我在外面散步时,偶然看到围栏里有一些绵羊,当时心想:“嘿,这简直就像词法环境嘛!”
让我解释一下:想象一个有绵羊在内的围栏区域。绵羊只能在围栏内活动,比如吃草。现在,想象围栏里还有一个小羊舍,小羊羔可以待在里面。羊舍里的小羊羔不能出去,但外面的大绵羊可以进去。
拆解这个类比
围栏代表了万物存在的整个区域——包括绵羊、羊羔、羊舍和草。这个被围栏圈起来的区域就是我们所说的 全局作用域。
在这个围栏区域内,羊舍是一个更小、独立的区域,代表一个 块级作用域。最后,绵羊吃的草(好吃好吃)就像是全局作用域里的一个 函数,也就是绵羊在这个空间里可以执行的一项特定活动或动作。
在代码块中:
- 全局作用域 由红框表示;
drinkWater函数作用域 由蓝框表示;if块级作用域 由绿框表示。
这三个就是词法环境。

全局作用域(围栏区域)
绵羊(由
var sheep = 1; 表示)象征着全局作用域中的一个变量,它可以在围栏区域内自由漫步。无论是在 drinkWater 函数外部还是内部,或者在 if 代码块内部,都可以使用它。函数作用域(drinkWater)
drinkWater 函数代表了绵羊在围栏区域内可以执行的一项动作。我们可以从全局作用域的任何地方调用 drinkWater 函数。然而,该函数本身在被定义时会创建一个新的词法环境。在这个函数内部,变量(比如 let waterTemperature = 'cold';)只能在函数内部被访问。块级作用域(if 代码块)
if 代码块创建了一个新的、更小的作用域。在这个由羊舍代表的作用域里,有 2 只小羊羔(let lambs = 2)。在这个作用域内部,一条 console.log 语句记录了 lambs 变量的值,同时也记录了全局的 sheep 变量。lambs 变量是该块级作用域特有的,而 sheep 变量则是从父级环境(全局作用域)中获取的。这之所以成为可能,要归功于 外部环境引用,它允许 JavaScript 沿着作用域链向上查找,并解析当前环境中找不到的变量。外部环境引用 是词法环境中的一个引用或指针。它指向父级词法环境,允许 JavaScript 通过查找作用域链来解析当前环境中找不到的变量。
提问时间!
你能修改
drinkWater() 函数,让它记录全局作用域中定义的可以喝水的绵羊总数吗?在评论区分享你的答案吧!理解多重词法环境
所以,我们可以看到这段代码中存在三个词法环境:全局作用域、函数作用域和块级作用域。当存在不止一个词法环境时,我们称之为多重词法环境。
理解这一点很重要:一段代码中可以同时存在多个词法环境。每次创建一个新的作用域(例如一个函数或一个代码块),就会生成一个新的词法环境。这意味着你代码的不同部分可以拥有各自独立的环境。
环境记录
现在我们已经理解了词法环境是如何工作的,让我们更深入地探讨环境记录的概念。
每当创建一个词法环境时——无论是为全局作用域、函数还是代码块——JavaScript 都会自动为其生成一个环境记录。
这个环境记录是一种数据结构,用于追踪该特定作用域内可访问的所有变量、函数和其他绑定。本质上,它充当了该环境中定义的所有内容的内部存储器,确保在代码执行期间需要时,可以获取到正确的数据。
词法环境与环境记录的区别
词法环境和环境记录之间的关键区别在于:
词法环境是 JavaScript 代码运行的场所。你可以把它想象成代码存在的“场景”或“上下文”。这个上下文包含了变量和函数的作用域,决定了在代码的任何一点,哪些变量或函数是可用的或可访问的。
例如,在我们的代码中,
lambs 变量只能在绿色边框的环境(块级作用域)内访问。此外,词法环境还包含一个外部环境引用(我们之前已经描述过),这允许它访问父级环境中的变量。环境记录则是词法环境内部的一个具体存储区域,用于保存该环境中使用的实际变量、函数声明和其他标识符。
如果说词法环境是更广泛的“上下文”,那么环境记录就是存储代码数据(如变量值和函数定义)的“仓库”。每当 JavaScript 需要访问一个变量或函数时,它都会在当前词法环境的环境记录中进行查找。
让我们再次使用代码示例来解释词法环境和环境记录:

这里有三个词法环境,每个环境都有自己的环境记录:
1. 全局作用域(红框),词法环境 1
这个词法环境是在全局层级创建的。此环境内的环境记录包含:
- 变量
sheep。 - 函数
drinkWater。 - 变量
sheepHouseInArea。
这些声明在整个代码中都是可访问的。全局环境还引用了在此环境中定义的函数
drinkWater,以及 if 语句(当执行时会创建其自己的块级作用域)。2. 函数作用域(drinkWater,蓝框),词法环境 2
此环境中的环境记录包含变量
waterTemperature,它是使用 let 在 drinkWater 函数内部声明的。这个变量只能在函数内部访问。然而,该函数也可以访问全局环境中的变量,比如 sheep。3. 块级作用域(if 代码块,绿框),词法环境 3
此环境内的环境记录包含变量
lambs,它是使用 let 在 if 代码块内部声明的。这个变量只能在这个特定的块级作用域内访问。该代码块也可以访问其父级环境中的变量,例如 sheep 和 sheepHouseInArea。内存中发生的幕后故事
在深入探讨了词法环境和环境记录之后,我们现在已经准备好去理解 JavaScript 在代码执行期间是如何管理内存和变量访问的了。
当你的代码运行时,JavaScript 会为每个函数或代码块创建一个新的词法环境。每个环境都有自己的环境记录,用于存储该作用域中定义的所有变量和函数。正如我们讨论过的,这种设置确保了内存的高效使用。
在幕后,JavaScript 引擎在内存中处理这些词法环境。调用栈用于跟踪函数调用,而块级作用域则创建新的词法环境,并将其链接到它们的外部环境。不过,与函数不同的是,这些块级作用域并不会被压入调用栈中。
什么是调用栈?
调用栈是 JavaScript 执行代码方式中的一个基础概念。
调用栈是一种数据结构,用于跟踪程序中的函数调用。它遵循后进先出的原则。它的工作原理如下:
- 当一个函数被调用时,它会被添加(压入)到栈的顶部。
- 当一个函数执行完毕时,它会被从栈的顶部移除(弹出)。
- 栈还会跟踪代码中的当前位置。
关于调用栈的关键点:
- 它记录了我们在程序中的位置。
- 如果我们进入一个函数,我们就把它放在栈顶。
- 如果我们从一个函数返回,我们就把它从栈中弹出。
- 栈有一个最大大小,如果超过了这个限制,就会导致“栈溢出”错误。

哈哈,这下你应该明白为什么会有“栈溢出”这个说法了吧!
来看一个简单的例子说明一下:
function greet(name) {
console.log('Hello, ' + name);
}
function processUser(user) {
greet(user);
}
processUser('Alice');
- main() (全局执行上下文)
- processUser('Alice')
- greet('Alice')
随着每个函数执行完毕,它们会依次从栈中弹出,直到我们回到全局上下文。
最后的终极提问!
在我们那个绵羊代码示例中,你能分辨出在执行过程中是否有任何东西被放入了调用栈吗?在评论区分享你的想法吧!
结语
第一部分就到这里!希望这篇文章能帮助你扎实地理解 JavaScript 是如何处理词法环境、环境记录,以及内存中发生的幕后故事的。在这个过程中我学到了很多,也希望你一样有所收获。如果你有任何问题或反馈,我很乐意听到你的声音——让我们一起学习和进步!
我把这篇文章定为“第一部分”,是因为我计划随后推出“第二部分”,届时我将深入探讨与词法环境紧密相关的三个主要概念:
- 闭包:你可以把它们想象成神奇的盒子,即使外部函数已经执行完毕,它们也能让函数记住并访问其外部环境中的变量。
- 作用域链:我们将探索 JavaScript 如何像寻宝一样,在嵌套的环境中穿梭以查找变量。
- 变量提升:这将解释为什么有些变量和函数似乎会“漂浮”到作用域的顶部,理解这一点可能有点棘手,但对于编写可预测的代码至关重要。
这些概念超级重要,因为它们直接影响你的 JavaScript 代码的行为方式。理解它们将帮助你编写更清晰、更高效的代码,并避免一些常见的陷阱。
敬请期待!

浙公网安备 33010602011771号