【译】学习JavaScript中提升、作用域、闭包的终极指南
这似乎令人惊讶,但在我看来,理解JavaScript语言最重要和最基本的概念是理解执行上下文。通过正确学习它,你将很好地学习更多高级主题,如提升,作用域链和闭包。考虑到这一点,究竟什么是“执行上下文”?为了更好地理解它,我们首先来看看我们如何编写软件。
编写软件的一种策略是将代码分解为单独的部分。虽然这些“部分”有许多不同的名称(功能,模块,包等),但它们都是为了一个目的而存在 – 分解和管理应用程序的复杂性。现在,不要像编写代码的人那样思考,而是根据JavaScript工具来解释代码。我们可以使用相同的策略,将代码分成几部分,管理解释代码的复杂性,就像我们编写代码一样吗?事实证明是可以的,而这些“部分”被称为执行上下文。就像函数/模块/包允许你管理编写代码的复杂性一样,执行上下文允许JavaScript引擎管理解释和运行代码的复杂性。现在我们知道了执行上下文的目的,我们需要回答的下一个问题是它们是如何创建的以及它们是由什么组成的?
JavaScript引擎运行代码时创建的第一个执行上下文称为“全局执行上下文”。最初这个执行上下文将包含两个东西 – 全局对象和一个被调用的变量this。this将引用全局对象,如果在浏览器中运行JavaScript,全局对象就是window对象,如果是在Node环境中运行,全局对象就是global。
上面我们可以看到,即使没有任何代码,全局执行上下文仍将包含两样东西 – window和this。这是最基本形式的全局执行上下文。
让我们一步一步,看看当我们开始实际向程序中添加代码时会发生什么。让我们从添加一些变量开始。
你能发现上面两张图片之间的差异吗?关键的一点是,每个执行环境都有两个独立的阶段,一个创建阶段(Creation)和一个执行阶段(Execution),每个阶段都有自己独特的职责。
在全局Creation阶段,JavaScript引擎将
创建一个全局对象。
创建一个名为“this”的对象。
为变量和函数设置内存空间。
在内存中放置任何函数声明时,为变量声明分配默认值“undefined”。
直到Execution阶段JavaScript引擎开始逐行运行代码并执行。
我们可以在下面的GIF中看到这个流程从一个Creation阶段到Execution另一个阶段。
在Creation阶段window和this创建过程中,变量声明(name和handle)被赋值为默认值undefined,并且任何函数声明(getUser)都完全放在内存中。然后,一旦我们进入Execution阶段,JavaScript引擎就会逐行开始执行代码,并将实际值分配给已经存在于内存中的变量。
GIF很酷,但不像单步执行代码并亲自查看过程一样酷。因为你应得的,我为你创建了JavaScript Visualizer。如果你想查看上面的代码,请使用此链接。
要真正理解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.logs时,Creation解析已经开始了。这意味着,正如我们之前看到的那样,变量声明应该被分配一个值undefined,而函数声明应该已经完全在内存中了。正如我们所期望的那样,name和handle的值都是是undefined,而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引擎会
创建一个全局对象。
创建一个名为“this”的对象。
为变量和函数设置内存空间。
在内存中放置任何函数声明时,为变量声明分配默认值“undefined”。
当我们谈论函数执行上下文时,哪些步骤没有意义?是第一步。在全局执行上下文Creation阶段创建时,我们应该只有一个全局对象,而不是每次调用函数时创建并且JavaScript引擎创建一个函数执行上下文。与全局执行上下文创建全局对象相反,函数执行上下文只需关注参数对象。考虑到这一点,我们可以调整我们之前的列表。每当创建一个函数执行上下文时,JavaScript引擎都会
1.创建一个全局对象。 (不同点,全局上下文特性)
1.创建一个参数对象。
2.创建一个名为this的对象。
3.为变量和函数设置内存空间。
4.在内存中放置任何函数声明时,为变量声明分配默认值“undefined”。
为了看到这一点,让我们回到我们之前的代码,但这一次,而不仅仅是定义getUser,让我们看看当我们调用它时会发生什么。
正如我们所讨论的那样,当我们调用getUser时,新的执行上下文就会创建。在getUsers执行上下文Creation阶段,JavaScript引擎创建一个this对象和一个arguments对象。因为getUser没有任何变量,JavaScript引擎不需要设置任何内存空间或“提升”任何变量声明。
你可能还注意到,当getUser函数执行完毕后,它将从可视化中删除。实际上,JavaScript引擎会创建所谓的“执行堆栈”(也称为“调用堆栈”)。无论何时调用函数,都会创建一个新的执行上下文并将其添加到执行堆栈中。每当函数完成同时运行Creation和Execution阶段时,它就会从执行堆栈中弹出。因为JavaScript是单线程的(意味着一次只能执行一个任务),所以这很容易可视化。使用“JavaScript Visualizer”,执行堆栈以嵌套方式显示,每个嵌套项目都是执行堆栈上的新执行上下文。
在这一点上,我们已经看到函数调用如何创建自己的执行上下文,这些执行上下文放在执行堆栈上。我们还没有看到的是局部变量如何发挥作用。让我们更改代码,以便我们的函数具有局部变量。
这里没有重要的细节需要注意。首先,你传入的任何参数都将作为本地变量添加到该函数的执行上下文中。在该示例中handle,作为全局执行上下文中的变量(因为它是定义它的位置)以及getURL执行上下文存在,因为我们将其作为参数传递。接下来是在函数内部声明的变量存在于该函数的执行上下文中。因此,我们创建的时候twitterURL,它存活在内部getURL执行上下文,因为这就是它的定义,不是在全局执行上下文。这似乎是显而易见的,但它是我们下一个主题作用域的基础。
在过去,你可能会听到“作用域”的定义,即“变量可访问的位置”。无论当时是否有意义,凭借你对执行上下文和JavaScript Visualizer工具的新发现,作用域将比以往更加清晰。实际上,MDN将“作用域”定义为“当前执行的上下文。”听起来很熟悉?我们可以以与我们如何考虑执行上下文非常相似的方式来思考“作用域”或“变量可访问的位置”。
这是对你的测试。bar当它记录在下面的代码中时会是什么?
function foo () {
var bar = 'Declared in foo'
}
foo()
console.log(bar)
让我们在JavaScript Visualizer中查看它。
当foo调用我们创建的执行堆栈一个新的执行上下文。该Creation阶段创建this,arguments并设置bar到undefined。然后Execution发生阶段并将字符串分配Declared in foo给bar。之后,Execution阶段结束,foo执行上下文从堆栈中弹出。一旦foo从执行堆栈中删除,我们就会尝试登录bar控制台。在那一刻,根据JavaScript Visualizer,它似乎bar从未存在过,所以我们得到了undefined。这向我们展示的是,在函数内部创建的变量是局部作用域的。这意味着(大多数情况下,我们稍后会看到异常)一旦从执行堆栈中弹出了函数的执行上下文,就无法访问它们。
这是另一个。代码执行完毕后将记录到控制台的内容是什么?
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 Visualizer。
我们得到undefined,Jordyn,Jake,然后Tyler。这告诉我们的是,你可以将每个新的执行上下文视为拥有自己独特的可变环境。即使存在包含变量的其他执行上下文,JavaScript引擎也将首先查看该变量的当前执行上下文。
这就提出了一个问题,如果变量在当前的执行上下文中不存在怎么办?JavaScript引擎会停止尝试查找该变量吗?让我们看一个能回答这个问题的例子。在下面的代码中,将记录什么?
var name = 'Tyler'
function logName () {
console.log(name)
}
logName(
你的直觉可能是它会输出记录,undefined因为logName执行上下文name在其范围内没有变量。这是公平的,但这是错误的。如果JavaScript引擎无法在函数的执行上下文中找到本地变量,它会查找该变量的最近父执行上下文。此查找链将一直持续到引擎到达全局执行上下文。在这种情况下,如果全局执行上下文没有变量,它将抛出一个引用错误。
如果本地执行上下文中不存在变量,则JavaScript引擎逐个进行并检查每个单独的父执行上下文的过程称为作用域链。JavaScript Visualizer通过使每个新的执行上下文缩进并具有唯一的彩色背景来显示范围链。在视觉上,你可以看到任何子执行上下文都可以引用位于其任何父执行上下文中的任何变量,但反之亦然。
之前我们了解到,在函数内部创建的变量是本地作用域的,一旦函数的执行上下文从执行堆栈中弹出,它们就不能(大部分)被访问。现在是时候深入研究“ 大部分 ”了。如果你有一个嵌套在另一个函数内的函数,那么这种情况并非如此。在这种情况下,即使从执行堆栈中删除了父函数的执行上下文,子函数仍然可以访问外部函数的作用域。那是很多话。与往常一样,JavaScript Visualizer可以帮助我们在这里。
请注意,在makeAdder执行堆栈中弹出执行上下文后,JavaScript Visualizer会创建所谓的闭包作用域。其中闭包作用域包含makeAdder执行上下文中存在的相同变量环境。发生这种情况的原因是因为我们有一个嵌套在另一个函数内部的函数。在我们的示例中,inner函数嵌套在函数内部makeAdder,因此在变量环境中inner创建。即使在执行堆栈中弹出执行环境之后,由于创建了执行堆栈,因此可以访问变量(通过作用域链)。ClosuremakeAddermakeAdderClosure Scopeinnerx
正如你可能猜到的那样,调用子函数“访问”其父函数的可变环境的概念称为闭包。
福利贴
以下是一些我知道的相关话题,如果我没有提及有人会打电话给我