【译】学习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
正如你可能猜到的那样,调用子函数“访问”其父函数的可变环境的概念称为闭包。
福利贴
以下是一些我知道的相关话题,如果我没有提及有人会打电话给我
【译】学习JavaScript中提升、作用域、闭包的终极指南的更多相关文章
- 认识javascript中的作用域和上下文
javascript中的作用域(scope)和上下文(context)是这门语言的独到之处,这部分归功于他们带来的灵活性.每个函数有不同的变量上下文和作用域.这些概念是javascript中一些强大的 ...
- 如何理解并学习javascript中的面向对象(OOP) [转]
如果你想让你的javascript代码变得更加优美,性能更加卓越.或者,你想像jQuery的作者一样,写出属于自己优秀的类库(哪怕是基于 jquery的插件).那么,你请务必要学习javascript ...
- 深入理解JavaScript中的作用域和上下文
介绍 JavaScript中有一个被称为作用域(Scope)的特性.虽然对于许多新手开发者来说,作用域的概念并不是很容易理解,我会尽我所能用最简单的方式来解释作用域.理解作用域将使你的代码脱颖而出,减 ...
- JavaScript中的作用域
很多(JavaScript)开发者都在讨论"作用域",但它是什么?它们在JavaScript中的任何地方!我发现很多年轻的开发者不知道作用域是什么.他们中大多数人可以用jQuery ...
- 【翻译】JavaScript中的作用域和声明提前
原文:http://www.adequatelygood.com/JavaScript-Scoping-and-Hoisting.html ===翻译开始=== 你知道下面的JavaScript脚本执 ...
- JavaScript中的作用域和声明提前
[翻译]JavaScript中的作用域和声明提前 原文:http://www.adequatelygood.com/JavaScript-Scoping-and-Hoisting.html ===翻译 ...
- 漫谈JavaScript中的作用域(scope)
什么是作用域 程序的执行,离不开作用域,也必须在作用域中才能将代码正确的执行. 所以作用域到底是什么,通俗的说,可以这样理解:作用域就是定义变量的位置,是变量和函数的可访问范围,控制着变量和函数的可见 ...
- 认识Javascript中的作用域和作用域链
作用域 只要写过java或者c#等语言的同学来说,相信一定能理解作用域的概念,在作用域的范围中,我们可以使用这个作用域的变量,对这个变量进行各种操作.可是,当使用Javascript的时候,相信很多的 ...
- JavaScript中的作用域与函数和变量声明的提升
var foo = 1; function bar() { if (!foo) { var foo = 10; } alert(foo); } bar(); ...
随机推荐
- DBUtils--数据库连接池
介绍 DBUtils是一套Python数据库连接池包,并允许对非线程安全的数据库接口进行线程安全包装. pg大概是是PostgreSQL(基于PyGreSQL)数据库,DB是其他数据库 Steady[ ...
- WM_COMMAND消息
原文地址:https://blog.csdn.net/whm243149796/article/details/78966065 当用户点击菜单.按钮.下拉列表框等控件时候,会触发WM_COMMAND ...
- [转]POI大数据量Excel解决方案
全文转载自:jinshuaiwang的博客 目前处理Excel的开源javaAPI主要有两种,一是Jxl(Java Excel API),Jxl只支持Excel2003以下的版本.另外一种是Apach ...
- 搭建python的虚拟环境
文章连接:https://www.cnblogs.com/zlsgh/p/8485848.html ubuntu系统下Python虚拟环境的安装和使用 前言:进行python项目开发的时 ...
- Android直连SQL Server数据库
1. 下载jtds,一个开放源代码的Java实现的JDBC驱动,地址:http://sourceforge.net/projects/jtds/ 2. 添加jtds到当前Android项目中,本人使用 ...
- android7.0以上使用融云即使通讯的坑
一.连接服务器不走connect()方法 在android6.0以下,在使用融云sdk时,直接将依赖库引入到项目中即可.但是在7.0及以上时,直接应用会发现消息一直发送不出去,错误提示为dlopen ...
- 布思算法——Java实现
前面一篇提到二进制队列实现了 N位二进制的补码,那么我们来实现布思算法. 关于BinaryQueue:https://www.cnblogs.com/XT-xutao/p/10050518.html ...
- 兼容性很好的纯css圆角
<!DOCTYPE HTML> <html lang="en-US"> <head> <meta charset="UTF-8& ...
- jQuery之jQuery扩展和事件
一.jQuery事件 常用事件 blur([[data],fn]) 失去焦点 focus([[data],fn]) 获取焦点( 搜索框例子) change([[data],fn]) 当select下拉 ...
- 【git】提交代码到远程仓库
看完不用,就是一个字:忘! 之前学了两天git结果今天要用的时候,啥也想不起来.... 场景: 已有远程仓库: git@192.168.1.1:test/test.git 要提交代码到远程仓库的新分支 ...