您现在的位置是:首页 > 文章详情

说说你对执行 JS 代码时产生的执行上下文的理解?

日期:2018-08-28点击:376

本文分两部分,第一部分是了解执行环境的相关概念,第二部分是通过是级代码了解具体的执行过程中执行环境的切换。

执行环境

执行环境的分类

1、全局执行环境

是js代码来时执行时的默认环境(浏览器中的window对象)。全局执行环境的变量对象始终都是作用域链中的最后对象。

2、函数执行环境

当某个函数被调用的时候,会先创建一个执行环境及相应的作用域链。然后使用arguments和其他命名参数的值来初始化执行环境变量对象。

3、使用eval()执行代码

没有块级作用域(本文不涉及es6的let等概念)

执行上下文(执行环境)的组成

执行环境(EC)或者称之为执行上下文,是JS中一个极为重要的概念,当javascript代码执行时,会进入不同的执行上下文,每一个执行上下文的组成基本如下:

img_4416fc9c2008a24d6f01cfa054886c44.png

变量对象:即包含变量的对象,除了我们无法访问她以外,和普通对象没有什么区别

[[Scope]]属性:数组。作用域链是一个由变量对象组成的带头节点的单项链表,其主要作用就是用来进行变量查询。而[[Scope]]属性是一个指向这个链表投节点的指针。

this:指向一个环境对象,注意是一个对象,而且是一个普通对象,而不是一个执行环境。

若干执行上下文会构成一个执行上下文栈(ECS),而所谓的执行上下文栈,举个例子,比如下边的代码:

img_1729419183db7172da79fc2313956ccc.png

代码首先进入GLOBAL Execution Context,然后依次进入outerFunc,innerFunc和foo的执行上下文,执行上下文栈就可以表示为

img_80987bec4bd89f18617d64c46df5ca92.png

执行全部代码时,会产生一个执行上下文环境,每次调用函数。当函数调用完成时,这个上下文环境以及其中的数据都会被消除,再回到全局上下文环境,处在活动状态的执行上下文环境只有一个。

img_3c8f37eace9655d287669858d8011e50.png

产生执行上下文的两个阶段

当一段js代码执行的时候,js解释器会通过两个阶段去产生一个EC

创建阶段(当函数被调用,但是开始执行函数内部代码之前)

    创建变量对象VO

    设置[[Scope]]属性的值

    设置this值

    激活/代码执行阶段

    初始化变量对象,即设置变量值、函数的引用、然后解释执行代码

创建变量对象VO过程

1、根据函数的参数,创建并初始化arguments object

2、扫描函数内部代码,查找函数声明(function declaration)

        对于所有找到的函数声明,将函数名和函数引用存入VO中

        如果VO中已经有同名函数了,那么进行覆盖

3、扫描函数内部代码,查找变量声明(variable declaration)

        对于所有找到的变量声明(通过var声明),将变量存入VO中,并初始化为undefied

        如果变量名跟已经声明的形参或者函数相同,则什么都不做

注:步骤2和3也称之为,声明提升

通过一段代码来了解javascript代码的执行

我们举例,假如我们有一个js文件,内容如下:

img_fe7d24525ba34cb49f08cac0d65fe09b.png

下面我们来一步一步说明解释器如何执行这段代码的:

1、创建全局上下文

首先,在解释器眼里 global_var1、global_sum叫全局变量,因为他们不属于任何函数。local_var1叫做局部变量,因为它定义在函数global_function1内部

global_function1叫做全局函数,因为它没有定义在任何函数内部

然后解释器开始扫描这段代码,为执行这段代码做一些准备工作——创建了一个全局上下文

全局上下文,可以把它看成是一个javascript对象,姑且称之为global_cintext。这个对象是解释器创建的,当然也是有解释器使用(我们的javascript代码是接触不到这个对象的)

global_context对象大概是这个样子的:

img_1735c88540fc864c58c2f854f544b662.png

可以看到,global_context有三个属性

Variable_Objext(VO)

img_77335ff12f31f6fa5e82e626698f8d04.png

解释器在VO中记录了变量全局变量global_var1、global_sum,但是他们的值现在是undefined的,还记录了全局函数global_function1,但是没有记录局部变量local_var1.VO的原型是Object.prototype.

Scope数组中的内容如下

[global_context.Variable_Object]

我们看到,Scope数组中只有一个对象,就是前边刚创建的VO

this

this的值现在是undefined

global_context对象被解释器压入一个栈中,不妨叫这个栈为context_stack,现在的context_stack是这样的:

img_bd650c401791afc3095800acad803b9d.png

创建出global_context后,解释器由偷偷干了一件事,他给global_function1设置了一个内部属性,也叫scope,他的值是global_context中的scope!也就是说,现在:

[ global_function1.scope ===  [ global_context.Variable_Object] ];

我们获取不到global_funciton1的scope属性的,只有解释器自己可以获取到。

2、逐行执行代码

解释器在创建了全局上下文之后,就开始执行这段代码了。

第一句  var global_var1 = 10;

解释器会把VO中的global_var1属性值设置为10.现在global_context对象就变成了这个样子:

img_3e68a7ebdba4c3e4852c9a7599a3afc4.png

第二句:

解释器继续执行我们的代码,他碰到声明式函数global_function1,由于在创建global_context对象时,她就已经记录好了该函数,所以它现在什么都不用做。

第三句

var global_sum = global_funcrion1(10)

解释器看到,我们在这里调用了函数global_fzaiunction1(解释器已经提前在global_context的VO中记录下了global_function1,所以它知道现在这里是一个函数的调用),并且传入了一个参数10,函数的返回结果赋值给了全局变量global_sum。

解释器并没有立即执行函数中的代码,因为它要为函数global_function1创建专门的context,我们叫她执行上下文(execute_context),因为每当解释器要执行一个函数的时候,都要创建一个类似的context.

execute_context也是一个对象,并且跟global——context还很像,下面是它里面的内容:

img_78294a82d14758e5ebf749835136c5b3.png

我们看到,execute_context与global_context相比,有以下几点变化:

*VO

首先记录了函数的形式参数parameter_a,并且给它赋值10,这个10就是我们调用函数的时候传递进去的。

然后记录了函数体内的局部变量local_var1,他的值是undefined.

然后时一个arguments属性,他的值是一个数组,里面只有一个10.

你可能疑惑,不是已经在parameter_a中记录了参数10了吗?,为什么解释器还要搞一个arguments,再来纪录一遍呢?原因是我们这样调用函数:

img_94a0a84ed9a79007a83f480276eef1d4.png

在javascript中是不违法的,此时在VO中的arguments会变成这样:arguments:[10,20,30]

parameter_a的值还是10.可见,arguments是专门记录我们穿进去的所有参数的。

*Scope

Scope属性仍然是一个数组,只不过里面的元素多了个execute_context.Variable_Object,并且排在了global_context.Variable_Object前面。

解释器是根据什么规则决定Scope中的内容的呢?答案非常简单:

img_4075984a92c884f472e35cc396032220.png

也就是说,每当要执行一个函数的时候,解释器就都将会执行上下文(execute_context)中Scope数组的第一个元素设为该执行上下文(execute_context)中VO对象,然后取出函数创建时保存在函数中的scope属性(本文中则是global_function1.scope),将其添加到执行上下文(execute_context)Scope数组的后面。

我们知道,global_function1是在global_context下创建的,创建的时候。他的scope属性被设置为global_context的Scope,里边只有一个global_context.Variable_Object,于是这个对象被添加到execute_context.Scope数组中execute_context.Variable_Object对象后面。

任何一个函数在创建的时候,解释器都会把它所在的执行上下文或者全局上下文Scope属性对应的数组设置给函数的scope属性,这个属性是函数与生俱来的。

*this

this的值仍然是undefined的(但是不同的解释器可能有不同的赋值)

解释器为函数global_function1创建好了execute_context(执行上下文)后,会把这个上下文对象压入context_stack中,所以,现在的xontext_stack是这样的:

img_ef5e9bd520492a7ff7f292490c83956f.png

准备执行函数内部的代码

做好了准备工作,解释器开始执行函数内部的代码,此时我们称函数是在执行上下文中运行的。

第一句

var local_var = 10;

它的处理办法很简单,将execute_context的VO中的local_var1赋值为10,这一点与在global_context下执行的变量赋值语句的处理一样,此时的execute_context变成这样:

img_542066eac5950867f8e317bb9d0e21d5.png

第二句

img_6f3299c2ec329ad7d97cbcb99b03b831.png

(1)解释器进一步考察语句,发现这是一个返回语句,于是它开始计算return 后面的表达式的值

(2)在表达式中他首先碰到了local_var1,它首先在execute_context的Scope中一次查找,在第一个元素中execute_context的VO发现了local_var1,并且知道它的值是10

(3)然后解释器继续前行,碰到了变量parameter_a,它如法炮制,在execute_context的VO中发现了parameter_a,并且确定它的值是10

(4)接着发现 global_var1,解释器从execute_context的Scope第一个元素execute_context.VO中查找,没有发现global_var1。继续查看Scope数组的第二个元素,即global_context.VO,发现并且确定了它的值为10。

(5)于是解释器将三个变量的值加的30,然后就返回

(6)此时,解释器知道函数已经执行完,那么他为这个韩式创建的执行上下文也没有用了,于是,它将execute_context从context_stack中弹出,由于没有其他对象引用着execute_context,解释器就把它销毁了,现在context_stack中只剩下了global_context.

第三句

img_be8fb7772c26ca9d3e0e3de13258abc5.png

现在解释器又回到全局上下文中执行代码了,这时它要把30赋值给sum,方法就是更改global_context中的VO对象global_sum属性值。

第四句

img_fd5e723d1c3e4aa35eb69d202e1e2079.png

解释器继续前进,碰到了语句alert(global_sum),很简单就是发出一个弹窗,弹窗的内容就是global_sum的值是30,当我们点击弹窗上的确定按钮的后,解释器知道,这段代码执行完了,他会打扫战场,把global_ssontext,context_stack等资源全部销毁。

再遇闭包

现在知道了上下文,函数的scope属性的知识后,我们可以开始学习闭包了,让我们把上线的js改成这样:

img_f5e229a95900465ea5416311ddf32148.png

这段代码与原先的代码最大的不同是,在global_function1内部,我们创建了一个函数local_function1,并且将它作为返回值。

当解释器执行函数global_function1时,仍然会为它创建执行上下文,只不过此时execute_context.VO中多了一个函数属性local_fucntion1.然后,解释器就会开始执行global_function1中的代码。

我们直接创建local_function1语句开始分析,看解释器怎么执行,闭包的所有秘密就隐藏在其中。

当解释器在execute_context中执行local_function1时,它仍然会将excute_context的Scope设置给函数local_fucntion1的scope属性,也就是这样:

local_function1.scope = [ execute_context.Variable_Object, global_context.Variable_Object ]

然后,解释器碰到了返回数据,把local_function1返回并赋值给全局变量global_sum.此时,函数global_function1已经执行完毕了,解释器会怎么处理它的execute_context呢?

首先,解释器会把execute_context从context_stack中弹出,但是不把它完全销毁,而是保留了execute_context.Variable_Object对象,把它转义到了另一个堆内存中,为什么不销毁呢?因为有对象要引用着它呢,因用链如下:

img_fb7e66245f46ccd9bb6747c271d48a90.png

这意味着什么呢?这说明,当local_function1结束返回后,它的形式参数parameter_a,局部变量local_var1以及局部函数local_function1都没有销毁,还仍然存在。这一点,与面向对宪法的源hjava中的经验完全不同,这也是闭包难以理解的根本。

下面我们的解释器继续执行语句alert(global_sum(10));alert参数是对函数global_sum的调用,global_sumde canshu wei 10,我们知道函数global_sum的代码是这样的:

img_33b0543514c6eadd8407d98e18afa756.png

要执行这个函数,解释器让然会为它创建上下文,我们姑且称之为local_context2,这个对象的内容是这样的:

img_faba317a533550f5f3c937852cb00a10.png

这里我们重点看看Scope属性,他的第一个元素毫无疑问是execute_context2.Variable_Object,后面的元素是从local_function1.scope属性中获得的,它是在local_function1创建时所在的执行上下文的Scope属性决定的。

创建execute_context2压入context_stack后,解释器开始执行语句

img_7ceea1f1167e643277a0e195b29e4bb2.png

对于该句中四个变量,解释器确定他们的值的办法一如既往的简单,首先在当前执行上下文(也就是execute_context2)的Scope的第一个元素中查找,第一个找不到就在第二个元素中查找,然后就是第三个,直到global_context.vARIABLE_oBJECT。

然后,解释器就会将四个变量值相加后返回,弹出execute_context2,此时execute_context2已经没有对象引用着它,解释器把它销毁了。

最后,alert函数会受到40,然后发出一个弹窗,弹窗的内容就是40,程序结束

说带现在,啥是闭包啊?

简单讲,当我们从函数global_function1中返回另一个函数local_function1时,由于local_function1的scope属性中应用着为执行global_function1创建的execute_context.Variable_Object,导致global_function1在执行完毕后,他的execute_context.Variable_Object并不会被收回,此时我们称函数local_function1是一个闭包,因为它除了是一个函数以外,还保存着创建它的执行上下文的变量信息,使得我们在调用它的时候,仍然能够访问这些变量。

函数将创建他的上下文中的VO对象封闭宝含在自己的scope属性中,函数鞠辉变成一个闭包,从这广泛的意义上来说,global_function1也就叫做闭包,因为他的scope内部属性也包含创建它的全局山下文的变量信息,也就是global_context.VO.

参考文章:https://www.jianshu.com/p/8f19e45fd1f1


推荐文章

教你步步为营掌握JavaScript闭包(本篇文章脱胎于这篇文章)

理解JS执行环境

深入理解JavaScript闭包和原型链

原文链接:https://yq.aliyun.com/articles/657636
关注公众号

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。

持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。

转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。

文章评论

共有0条评论来说两句吧...

文章二维码

扫描即可查看该文章

点击排行

推荐阅读

最新文章