博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
【译】终极指南:变量提升、作用域和闭包
阅读量:6800 次
发布时间:2019-06-26

本文共 5682 字,大约阅读时间需要 18 分钟。

  • 原文作者:
  • 原文链接:
  • 文中部分链接可能需要梯子。
  • 欢迎批评指正。

说出来可能吓你一跳,在我看来,理解Javascript的最重要最基本的思路就是理解执行上下文。吃透了执行上下文,你就能更好地学习诸如变量提升、作用域链和闭包等进阶知识。说到这个,到底什么是“执行上下文”?为了更好理解,我们先来看一看我们是怎么写代码的。

编程的一个策略就是把代码拆分开。虽然那些拆开的“零件”有不同的名字(函数、方法、包等等),它们都是为了一个目的而存在——降低应用的复杂度,便于管理。现在,抛开开发者的思维,设想你是解析代码的Javascript引擎,这种情景下,我们能像写代码时候那样,用相同的策略拆分代码来解析代码吗?事实证明我们可以,这些“零件”就叫做执行上下文。就像函数/模块/包等能帮你进行复杂的开发,执行上下文帮助Javascript引擎管理整个解析和运行代码的复杂过程。那么现在我们了解了执行上下文的存在目的,下一个问题就是执行上下文是怎么创建的?它们由什么组成?

当Javascript引擎运行代码,第一个被创建的执行上下文叫做“全局执行上下文”。最初,这个全局上下文由这二位组成:一个全局对象和一个this变量。this引用的是全局对象,如果在浏览器中运行Javascript,那么这个全局对象就是window对象,如果在Node环境中运行,这个全局对象就是global对象。

从上图可以看出,即使没有任何代码,全局执行上下文中仍然有windowthis。这就是最基本的全局执行上下文。

让我们看看添加了代码会怎么样:

能看出上面两张图的区别吗?关键在于每个执行上下文有两个独立的阶段,一个是创建阶段,一个是执行阶段,每个阶段都有其各自职责。

在全局执行上下文的创建阶段,Javascript引擎会:

1. 创建一个全局对象;2. 创建this对象;3. 给变量和函数分配内存;4. 给变量赋默认值undefined,把所有函数声明放进内存。复制代码

直到执行阶段,Javascript引擎才会一行一行地运行你的代码并执行它们。

通过下面的动图我们可以看到从创建阶段到执行阶段的流程:

在创建阶段,windowthis被创建出来,变量声明被设为默认值undefined,所有函数声明都被存入内存。一旦进入执行阶段,Javascript引擎就开始一行行执行代码,把内存中已经存在的变量赋予真实值。

动图确实很炫酷,但也不如你手敲一遍,亲自体会这个处理过程。你需要一个工具,所以我创建了。如果你想过一遍例子中的代码,可以用。

为了切实巩固创建阶段和执行阶段的概念,让我们在控制台打印一些处于创建之后执行之前的值来看看:

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(),创建阶段就已经发生了。这意味着正如我们之前所见,变量声明早已被赋予了默认值undefined,同时函数声明已经在内存中就绪。在例子中,namehandle的值是undefinedgetUser也正是内存中的函数的引用。

console.log('name: ', name) // name: undefinedconsole.log('handle: ', handle) // handle: undefinedconsole.log('getUser :', getUser) // getUser: ƒ getUser () {}var name = 'Tyler'var handle = '@tylermcginnis'function getUser () {  return {    name: name,    handle: handle  }}复制代码

译者注:本人实操代码的结果与原作者的结果有出入,见下图:

且将变量name改为其他字符串,打印的结果如下

在创建阶段将变量声明赋予默认值的过程就叫做变量提升

是不是有恍然大悟的感觉?可能之前对变量提升的理解不是很清晰。关于变量提升让你困惑之处在于,没有谁真的被“提升”或者移动了。现在你理解了执行上下文,理解了变量声明在创建阶段被赋予默认值,那你就理解了“提升”,因为那完全就是字面意思。


此刻你应该对全局执行上下文和它的两个阶段一点都不感觉别扭了。好消息是,你只需再学习另一个执行上下文就够了,而且它和全局执行上下文几乎一样。它叫做函数执行上下文,当函数被调用,它就被创建出来了。

再重申一遍关键:仅当Javascript引擎首次开始解析代码(对应全局执行上下文)或当一个函数被调用时,才会创建执行上下文。

现在我们需要搞清楚的主要问题就是,全局执行上下文和函数执行上下文有什么区别。回想一下,我们之前学到过,在全局创建阶段,Javascript引擎会:

1. 创建一个全局对象;2. 创建this对象;3. 给变量和函数分配内存;4. 给变量赋默认值undefined,把所有函数声明放进内存。复制代码

现在换成函数执行上下文,想想看,哪个步骤就对不上号了呢?对,就是第一步。我们有一个全局对象就够了,那就是在全局执行上下文的创建阶段所创建的那个,而不是每次函数调用都创建一个。函数执行上下文中应该创建的应该是arguments对象,所以当创建函数执行上下文时,Javascript引擎会:

1.创建一个全局对象

1.创建一个arguments对象;

2. 创建this对象;;
3. 给变量和函数分配内存;;
4. 给变量赋默认值undefined,把所有函数声明放进内存。

让我们回过头看看之前的代码,但这次我们不仅仅定义getUser,还要调用一次,看看实际效果是什么。

正如我们所说,当调用了getUser,就创建了新的执行上下文。在getUser执行上下文的创建阶段的创建阶段,Javascript引擎创建了this对象和arguments对象。getUser没有任何变量,所以Javascript引擎不需要再次分配内存或进行“提升”。

你可能注意到了,当getUser函数执行完毕,它就从中消失了。事实上,Javascript引擎创建了一个叫“执行栈”(也叫调用栈)的东西。每当函数被调用,就创建一个新的执行上下文并把它加入到调用栈;每当一个函数运行完毕,就被从调用栈中弹出来。因为Javascript是单线程的,通过能看到,每一个新的执行上下文都嵌套在另一个中,形成了调用栈。


现在我们知道了函数调用是如何创建它们各自的执行上下文并放到调用栈中的。但我们没有看到局部变量是如何作用的,那就让我们来改写之前的代码,让函数拥有局部变量。

这里有几处重要细节需要注意。首先,传入函数的所有参数都作为局部变量存在于该函数的执行上下文中。在例子中,handle同时存在与全局执行上下文和getURL执行上下文中,因为我们把它传入了getURL函数做为参数。其次,在函数中声明的变量存在于函数的执行上下文中。所以当我们创建twitterURL,它就会存于getURL执行上下文中。这看起来显而易见,但却是我们下一个话题——作用域——的基础。


可能你以前就听说过作用域的定义“变量可访问之处”。不管当时你是如何理解的,现在结合你新学的知识和工具,作用域这个概念会在你脑海里更清晰。MDN把作用域定义为“执行的当前上下文”。是不是耳熟?我们可以把作用域看作是“变量可访问之处”,正如我们理解执行上下文那样。

这里有一个小测试。下面代码中,打印出来的bar将会是什么?

function foo(){    var bar='Declared in foo';}foo();console.log(bar);复制代码

让我们到Javascript Visualizer中看看:

当我们调用了foo,就在调用栈中新增了一个执行上下文。在其创建阶段,产生了thisargumentsbar被设为undefined。然后到了执行阶段,把字符串'Declare in foo'赋予bar。到这里执行阶段就结束了,foo执行上下文从调用栈弹出。foo弹出后,代码就运行到了打印bar到控制台的部分。此刻,根据所展示的状态,bar似乎根本不存在,因此我们得到的是undefined。(译者注:实际上运行这个例子会报错:Uncaught ReferenceError: bar is not defined)这告诉我们,在函数中创建的变量,它的作用域是局部的。这意味着(通常如此,后面会讲例外)一旦函数的执行上下文从调用栈弹出,该函数中声明的变量就访问不到了。

再看一个例子。代码执行完毕后控制台会打印出什么?

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);复制代码

控制台会依次打印出undefinedJordynJakeTyler。你可以这么想:每个新的执行上下文都有它自己的变量环境。就算另有其他执行上下文包含变量name,Javascript引擎仍会先从当前执行上下文里找起。

这就带来一个问题,要是当前执行上下文里没有要找的变量呢?Javascript会就此罢手吗?下面的例子里有答案。

var name='Tyler';function logName(){	console.log(name);}logName();复制代码

你的直觉可能会是:既然在logName的执行上下文中找不到name变量,那肯定打印出undefined。其实不然。如果Javascript引擎在函数执行上下文找不到匹配的局部变量,它会到最接近的父级上下文中查找。这条查找链会一直延伸到全局执行上下文。如果此时仍然找不到该变量,Javascript引擎就会抛出一个引用错误。

每逢当前执行上下文中找不到所需变量,Javascript引擎就向上逐级查找,这个处理过程就是作用域链。Javascript Visualizer通过把每个执行上下文表示为不同颜色的区域并按层级缩进,来描述作用域链。你能直观体会到,子级执行上下文可以引用父级执行上下文中声明的变量,但反过来就不行。


之前我们了解到函数中创建的变量仅局部有效,一旦函数执行上下文从调用栈弹出,这些变量就访问不到了(通常如此)。现在是时候研究一下不在“通常如此”范围的情况了。如果你在一个函数中嵌入了另一个函数,例外情况就产生了。这种函数套函数的情况下,即使父级函数的执行上下文从调用栈弹出了,子级函数仍然能够访问父级函数的作用域。啰嗦了一堆,我们还是用Javascript Visualizer看看吧。

var count=0;function makeAdder(x){    return function inner(y){        return x+y;    }}var add5=makeAdder(5);count+=add5(2);复制代码

注意,makeAdder执行上下文从调用栈弹出后,Javascript Visualizer创建了一个Closure Scope(闭包作用域)Closure Scope中的变量环境和makeAdder执行上下文中的变量环境相同。这是因为我们在函数中嵌入了另一个函数。在本例中,inner函数嵌在makeAdder中,所以innermakeAdder变量环境的基础上创建了一个闭包。因为闭包作用域的存在,即使makeAdder已经从调用栈弹出了,inner仍然能够访问到x变量(通过作用域链)。

你可能已经猜到了,这种子函数在其父级函数的变量环境上“关闭”(译者注:原文为a child function “closing” over the variable environment of its parent function)的概念,就叫做闭包


福利部分

下面是一些相关话题,我知道如果我不提及的话,肯定会有人揪我出来补充。

全局变量

在浏览器中,你在全局执行上下文中(不被任何函数包裹)创建的变量,都会成为window对象的属性。

在浏览器和Node环境中,如果你不声明(比如使用var/let/const)就直接创建了一个变量,这个变量同样会成为全局对象的属性。

// In the browservar name = 'Tyler'function foo () {  bar = 'Created in foo without declaration'}foo()console.log(window.name) // Tylerconsole.log(window.bar) // Created in foo without declaration复制代码

let和const

this

在本文中,我们了解到,每个执行上下文的创建阶段中,Javascript引擎都会创建一个叫做this的对象。如果你想深入学习关于this的知识,我建议你读读。

转载地址:http://mcywl.baihongyu.com/

你可能感兴趣的文章
Http、TCP/IP协议与Socket之间的区别
查看>>
ARM工控主板在驾考驾培智能终端的使用
查看>>
大数据开启“互联网+统计”新模式
查看>>
文思海辉:智慧数据避免企业成为大数据时代落伍者
查看>>
什么!建设数据中心还得看风水?
查看>>
如何通过SSH隧道实现 Windows Pass the Ticket攻击?
查看>>
破解“动物农场”高级间谍平台Dino
查看>>
隐私安全新动向:Facebook采用OpenPGP加密技术
查看>>
食品巨头康尼格拉:数据分析如何影响企业成本?
查看>>
迅雷发布“星域CDN” 做条颠覆市场的鲶鱼
查看>>
多租户特性一定是SaaS软件的必要特征吗?
查看>>
如何在Ubuntu中安装语音聊天工具Discord
查看>>
数据可视化,我应从何开启?
查看>>
黑客入侵凯特王妃妹妹账号盗数千照片 欲卖给媒体
查看>>
【人生苦短,我用Python】Python免费精品课连载(1)——Python入门
查看>>
IBM向认知转型 选择混合云路径
查看>>
英国《数字经济法案》
查看>>
必须了解的五个光伏发电财务和税收政策
查看>>
思默特获评“用户满意服务奖”荣誉
查看>>
CYQ.DBImport 数据库反向工程及批量导数据库工具 V1.0 发布
查看>>