JavaScript 执行上下文、提升、作用域和闭包终极指南
这是我们高级 JavaScript课程的一部分。如果您喜欢这篇文章,请查看完整课程。
或许你会感到惊讶,但我认为理解 JavaScript 语言最重要、最根本的概念是理解执行上下文。掌握了执行上下文,你就能更好地学习更高级的主题,例如提升、作用域链和闭包。那么,究竟什么是“执行上下文”呢?为了更好地理解它,我们首先来看看软件的编写方式。
编写软件的一种策略是将代码拆分成多个独立的部分。虽然这些“部分”名称各异(函数、模块、包等等),但它们的存在只有一个目的——分解和管理应用程序的复杂性。现在,让我们换个角度思考,不要再像编写代码的人那样思考,而是站在 JavaScript 引擎的角度来思考,它的职责是解释代码。我们能否像编写代码那样,运用同样的策略,将代码拆分成多个部分,来管理解释代码的复杂性呢?答案是肯定的,这些“部分”被称为执行上下文(Execution Context)。正如函数/模块/包可以帮助你管理编写代码的复杂性一样,执行上下文也可以帮助 JavaScript 引擎管理解释和运行代码的复杂性。既然我们已经了解了执行上下文的用途,接下来我们需要回答的问题是:它们是如何创建的?它们由什么组成?
JavaScript 引擎运行代码时创建的第一个执行上下文称为“全局执行上下文”。最初,此执行上下文包含两部分:一个全局对象和一个名为 `.` 的变量this。`.`会引用该全局对象,如果您在浏览器中运行 JavaScript,则该对象为this`<global_object>`;如果您在 Node 环境中运行 JavaScript,则该对象为 `<global_object>`。windowglobal
如上所示,即使没有任何代码,全局执行上下文仍然包含两部分——window和this。这是全局执行上下文最基本的形式。
让我们更进一步,看看当我们真正开始向程序中添加代码时会发生什么。我们先从添加几个变量开始。
你能看出上面两张图片的区别吗?关键在于,每个执行上下文都包含两个独立的阶段:一个Creation阶段和一个Execution阶段,每个阶段都有其独特的职责。
在全局Creation阶段,JavaScript引擎将
1) 创建一个全局对象。2
) 创建一个名为“this”的对象。3
) 为变量和函数分配内存空间。4
) 将变量声明的默认值设置为“undefined”,同时将函数声明放入内存中。
Execution直到JavaScript 引擎开始逐行运行并执行你的代码的阶段才会发生。
我们可以在下面的 GIF 动画中看到这一Creation过程。Execution
在创建变量的阶段,变量声明(`a`和Creation` b` )会被赋予默认值 `null` ,所有函数声明(`f` )都会被完全加载到内存中。然后,一旦进入执行阶段,JavaScript 引擎就会逐行执行代码,并将实际值赋给已经存在于内存中的变量。windowthisnamehandleundefinedgetUserExecution
GIF 动画很酷,但远不及亲眼查看代码、了解其运行过程来得震撼。为了让您体验更直观的感受,我特地为您打造了JavaScript 可视化工具。如果您想查看上面这段代码的完整运行过程,请点击此链接。
Creation为了真正巩固阶段与阶段的概念Execution,让我们记录阶段之后Creation和阶段之前的Execution一些值。
console.log('name: ', name)
console.log('handle: ', handle)
console.log('getUser :', getUser)
var name = 'Tyler'
var handle = '@tylermcginnis'
function getUser () {
return {
name: name,
handle: handle
}
}
在上面的代码中,你期望控制台输出什么?当 JavaScript 引擎开始逐行执行代码并调用 `console.log` 时,该Creation阶段已经完成。这意味着,正如我们之前看到的,变量声明应该已经被赋值,undefined而函数声明应该已经完全加载到内存中。因此,正如我们所预期的,` a`name和` handleb` 都是`a`,undefined而 `c`getUser是指向内存中函数的引用。
console.log('name: ', name) // name: undefined
console.log('handle: ', handle) // handle: undefined
console.log('getUser :', getUser) // getUser: ƒ getUser () {}
var name = 'Tyler'
var handle = '@tylermcginnis'
function getUser () {
return {
name: name,
handle: handle
}
}
在创建阶段为变量声明分配默认值的这个过程
undefined称为提升。
希望你刚才恍然大悟。你可能之前听人解释过“提升”的概念,但并没有真正理解。“提升”令人困惑的地方在于,实际上并没有任何东西被“提升”或移动。现在你理解了执行上下文,也明白了变量声明undefined在Creation阶段会被赋予默认值,那么你就理解了“提升”,因为它本质上就是这样。
到目前为止,你应该已经比较熟悉全局执行上下文及其两个阶段Creation了Execution。好消息是,你只需要学习另一个执行上下文,它几乎与全局执行上下文完全相同。它被称为函数执行上下文,每当函数被调用时都会创建它。
这一点至关重要。只有在 JavaScript 引擎首次开始解释你的代码时(全局执行上下文)以及每次调用函数时,才会创建执行上下文。
现在我们需要回答的主要问题是:全局执行上下文和函数执行上下文之间有什么区别?如果你还记得之前的内容,我们说过,在全局Creation阶段,JavaScript 引擎会……
1) 创建一个全局对象。2
) 创建一个名为“this”的对象。3
) 为变量和函数分配内存空间。4
) 将变量声明的默认值设置为“undefined”,同时将函数声明放入内存中。
在讨论函数执行上下文时,哪个步骤不合理Creation?是第一步。我们应该只在全局执行上下文阶段创建一个全局对象,而不是每次函数被调用时 JavaScript 引擎都创建一个函数执行上下文。函数执行上下文需要关注的全局对象与全局执行上下文不同的一点是参数。考虑到这一点,我们可以调整之前的列表。每当创建一个函数执行上下文时,JavaScript 引擎都会……
1.
创建全局对象。
1. 创建 arguments 对象。2
. 创建一个名为 this 的对象。3
. 为变量和函数分配内存空间。4
. 将变量声明的默认值设置为“undefined”,同时将函数声明放入内存中。
为了看看实际效果,让我们回到之前的代码,但这次我们不只是定义它getUser,而是看看调用它时会发生什么。
正如我们之前讨论的,当我们调用时,getUser会创建一个新的执行上下文。在执行上下文Creation阶段getUsers,JavaScript 引擎会创建一个this对象和一个arguments对象。由于getUser该对象不包含任何变量,因此 JavaScript 引擎无需设置任何内存空间或“提升”任何变量声明。
您可能也注意到,当getUser函数执行完毕后,它会从可视化界面中移除。实际上,JavaScript 引擎会创建一个称为“执行栈”(也称为“调用栈”)的东西。每次调用函数时,都会创建一个新的执行上下文并添加到执行栈中。当函数完成所有阶段(包括回调Creation和执行)后Execution,它会从执行栈中弹出。由于 JavaScript 是单线程的(意味着一次只能执行一个任务),因此很容易将其可视化。“JavaScript 可视化工具”会以嵌套的方式显示执行栈,每个嵌套项都是执行栈上的一个新的执行上下文。
到目前为止,我们已经了解了函数调用如何创建自己的执行上下文,并将其放置在执行栈上。但我们还没有看到局部变量是如何参与其中的。让我们修改代码,使函数拥有局部变量。
这里有几个重要的细节需要注意。首先,你传入的任何参数都会作为局部变量添加到该函数的执行上下文中。在示例中,handle它既存在于Global执行上下文中(因为它是在那里定义的),也存在getURL于执行上下文中(因为我们把它作为参数传入了)。其次,在函数内部声明的变量存在于该函数的执行上下文中。所以,当我们创建变量时twitterURL,它存在于getURL执行上下文中,因为它是在那里定义的,而不是在执行Global上下文之外。这看起来似乎很明显,但它是我们下一个主题“作用域”的基础。
过去你可能听说过“作用域”的定义,比如“变量可访问的位置”。无论当时你是否理解,现在有了执行上下文和 JavaScript 可视化工具的新知识,作用域的概念将会比以往任何时候都更加清晰。事实上,MDN 将“作用域”定义为“当前的执行上下文”。听起来是不是很熟悉?我们可以用理解执行上下文的方式来理解“作用域”或“变量可访问的位置”。
这里有一个测试题。请问bar以下代码执行后会输出什么内容?
function foo () {
var bar = 'Declared in foo'
}
foo()
console.log(bar)
让我们在 JavaScript 可视化工具中查看一下。
当foo调用 `get` 函数时,我们会在执行栈上创建一个新的执行上下文。`get`Creation阶段会创建this`__init__`、arguments`__init__` 和 `__init__`,并将bar`__init__` 的值设置为 `__init__` 。undefined然后Execution执行 `get` 阶段,并将字符串赋值给 `__init__` 。之后,` get` 阶段结束,执行上下文从栈中弹出。一旦 `__init__`从执行栈中移除,我们尝试向控制台输出日志。此时,根据 JavaScript Visualizer 的显示,它就好像从未存在过一样,因此我们得到 `__init__` 。这表明,在函数内部创建的变量是局部作用域的。这意味着(大多数情况下是这样,稍后我们会看到一个例外),一旦函数的执行上下文从执行栈中弹出,这些变量就无法被访问。Declared in foobarExecutionfoofoobarbarundefined
还有一个问题。代码执行完毕后,控制台会记录什么内容?
function first () {
var name = 'Jordyn'
console.log(name)
}
function second () {
var name = 'Jake'
console.log(name)
}
console.log(name)
var name = 'Tyler'
first()
second()
console.log(name)
我们再来看看 JavaScript 可视化工具。
我们得到undefined,,,然后。这表明,您可以将每个新的执行上下文视为拥有其自身独特的变量环境。即使其他执行上下文中包含该变量Jordyn,JavaScript 引擎也会首先在当前执行上下文中查找该变量。JakeTylername
这就引出了一个问题:如果变量在当前执行上下文中不存在会怎样?JavaScript 引擎会停止查找该变量吗?让我们来看一个例子来解答这个问题。在下面的代码中,会记录什么内容?
var name = 'Tyler'
function logName () {
console.log(name)
}
logName()
undefined你可能直觉地认为,由于logName执行上下文的作用域内没有该变量,所以会记录日志name。这种想法没错,但却是错误的。实际情况是,如果 JavaScript 引擎在函数的执行上下文中找不到某个局部变量,它会向上查找最近的父级执行上下文。这个查找链会一直进行下去,直到引擎到达全局执行上下文。在这种情况下,如果全局执行上下文中也没有该变量,就会抛出一个引用错误。
JavaScript 引擎会逐个检查每个父级执行上下文,确认变量在本地执行上下文中是否存在,这个过程称为作用域链
Scope Chain。JavaScript 可视化工具通过缩进每个新的执行上下文并使用不同的背景颜色来展示作用域链。从视觉上看,任何子执行上下文都可以引用其任何父级执行上下文中的任何变量,反之则不然。
我们之前了解到,函数内部创建的变量是局部作用域的,一旦函数的执行上下文从执行栈中移除,这些变量(大多数情况下)就无法被访问。现在是时候深入探讨一下这个“大多数情况下”了。有一种例外情况:当一个函数嵌套在另一个函数中时。在这种情况下,即使父函数的执行上下文已从执行栈中移除,子函数仍然可以访问外部函数的作用域。以上内容有点长。和往常一样,JavaScript Visualizer 可以帮助我们理解。
请注意,在makeAdder执行上下文从执行栈中弹出后,JavaScript Visualizer 会创建一个称为“作用域”的组件Closure Scope。该组件内部Closure Scope包含与执行上下文中相同的变量环境makeAdder。出现这种情况的原因是,我们有一个函数嵌套在另一个函数中。在我们的示例中,该inner函数嵌套在另一个函数中makeAdder,因此inner会创建一个作用域来Closure访问makeAdder变量环境。即使makeAdder执行上下文已从执行栈中弹出,由于该组件Closure Scope已被创建,因此它仍然可以通过作用域链inner访问该变量。x
正如你可能猜到的那样,子函数“关闭”其父函数的变量环境的概念被称为Closures。
奖励部分
以下是一些相关的话题,我知道如果我不提的话,肯定会有人指出我的错误🙈。
全局变量
在浏览器中,无论何时在全局执行上下文(任何函数之外)中创建变量,该变量都会作为属性添加到对象中window。
在浏览器和 Node 中,如果您创建了一个没有声明的变量(即没有 ` var,`、let`,` 或const`),该变量也会被添加为全局对象的属性。
// In the browser
var name = 'Tyler'
function foo () {
bar = 'Created in foo without declaration'
}
foo()
console.log(window.name) // Tyler
console.log(window.bar) // Created in foo without declaration
令和常量
这个关键词
本文中我们了解到,在Creation每个执行上下文阶段,JavaScript 引擎都会创建一个名为 `this` 的对象this。如果您想了解更多关于这为何重要以及如何确定关键字this的信息,我建议您阅读《这是什么?——理解 JavaScript 中的 `this` 关键字、`call`、`apply` 和 `bind`》。
本文最初发表于TylerMcGinnis.com,是其高级 JavaScript课程的一部分。
文章来源:https://dev.to/tylermcginnis/the-ultimate-guide-to-execution-contexts-hoisting-scopes-and-closures-in-javascript-4bih


