JavaScript的执行过程(深入执行上下文、GO、AO、VO和VE等概念)
<h2 id="javascript的执行过程">JavaScript的执行过程</h2><h3 id="前言">前言</h3>
<p>编写一段JavaScript代码,它是如何执行的呢?简单来说,JS引擎在执行JavaScript代码的过程中需要先解析再执行。那么在解析阶段JS引擎又会进行哪些操作,接下来就一起来了解一下JavaScript在执行过程中的详细过程,包括执行上下文、GO、AO、VO和VE等概念的理解。</p>
<h3 id="1初始化全局对象">1.初始化全局对象</h3>
<blockquote>
<p>首先,JS引擎会在执行代码之前,也就是解析代码时,会在我们的堆内存创建一个全局对象:Global Object(简称GO),观察以下代码,在全局中定义了几个变量:</p>
</blockquote>
<p>示例代码:</p>
<pre><code class="language-js">var name = 'curry'
var message = 'I am a coder'
var num = 30
</code></pre>
<p>JS引擎内部在解析以上代码时,会创建一个全局对象(伪代码如下):</p>
<ul>
<li>所有的<strong>作用域(scope)</strong>都可以访问该全局对象;</li>
<li>对象里面会包含一些<strong>全局的方法和类</strong>,像Math、Date、String、Array、setTimeout等等;</li>
<li>其中有一个<strong>window属性</strong>是指向该全局对象自身的;</li>
<li>该对象中会收集我们上面全局定义的变量,并设置成undefined;</li>
<li>全局对象是非常重要的,我们平时之所以能够使用这些全局方法和类,都是在这个全局对象中获取的;</li>
</ul>
<pre><code class="language-js">var GlobalObject = {
Math: '类',
Date: '类',
String: '类',
setTimeout: '函数',
setInterval: '函数',
window: GlobalObject,
...
name: undefined,
message: undefined,
num: undefined
}
</code></pre>
<h3 id="2执行上下文栈调用栈">2.执行上下文栈(调用栈)</h3>
<blockquote>
<p>了解了什么是全局对象后,下面就来聊聊代码具体执行的地方。JS引擎为了执行代码,引擎内部会有一个<strong>执行上下文栈(Execution Context Stack,简称ECS)</strong>,它是用来执行代码的<strong>调用栈</strong>。</p>
</blockquote>
<p><strong>(1)ECS如何执行?先执行谁呢?</strong></p>
<ul>
<li>无疑是先执行我们的全局代码块;</li>
<li>在执行前全局代码会构建一个<strong>全局执行上下文(Global Execution Context,简称GEC)</strong>;</li>
<li>一开始GEC就会被放入到ECS中执行;</li>
</ul>
<p><strong>(2)那么全局执行上下文(GEC)包含那些内容呢?</strong></p>
<ul>
<li><strong>第一部分:</strong>执行代码前。
<ul>
<li>在转成抽象语法树之前,会将全局定义的变量、函数等加入到Global Object中,也就是上面初始化全局对象的过程;</li>
<li>但是并不会真正赋值(表现为undefined),所以这个过程也称之为<strong>变量的作用域提升(hoisting)</strong>;</li>
</ul>
</li>
<li><strong>第二部分:</strong>代码执行。
<ul>
<li>对变量进行赋值,或者执行其它函数等;</li>
</ul>
</li>
</ul>
<p>下面就通过一幅图,来看看GEC被放入ECS后的表现形式:</p>
<p><img src="https://img2020.cnblogs.com/blog/2506425/202201/2506425-20220110193958112-823699983.png" alt="" loading="lazy"></p>
<h3 id="3调用栈调用gec的过程">3.调用栈调用GEC的过程</h3>
<blockquote>
<p>接下来,将全局代码复杂化一点,再来看看调用栈调用全局执行上下文(GEC)的过程。</p>
</blockquote>
<p>实例代码:</p>
<pre><code class="language-js">var name = 'curry'
console.log(message)
var message = 'I am a coder'
function foo() {
var name = 'foo'
console.log(name)
}
var num1 = 30
var num2 = 20
var result = num1 + num2
foo()
</code></pre>
<p>调用栈调用过程:</p>
<ul>
<li>
<p>1.初始化全局对象。</p>
<ul>
<li>这里需要注意的是函数存放的是地址,会指向函数对象,与普通变量有所不同;</li>
<li>从上往下解析JS代码,当解析到foo函数时,因为foo不是普通变量,并不会赋为undefined,JS引擎会在堆内存中开辟一块空间存放foo函数,在全局对象中引用其地址;</li>
<li>这个开辟的函数存储空间最主要存放了该函数的<strong>父级作用域</strong>和函数的<strong>执行体代码块</strong>;</li>
</ul>
<p><img src="https://img2020.cnblogs.com/blog/2506425/202201/2506425-20220110194020446-732333674.png" alt="" loading="lazy"></p>
</li>
<li>
<p>2.构建一个全局执行上下文(GEC),代码执行前将VO的内存地址指向GlobalObject(GO)。</p>
<p><img src="https://img2020.cnblogs.com/blog/2506425/202201/2506425-20220110194047108-1015815567.png" alt="" loading="lazy"></p>
</li>
<li>
<p>3.将全局执行上下文(GEC)放入执行上下文栈(ECS)中。</p>
<p><img src="https://img2020.cnblogs.com/blog/2506425/202201/2506425-20220110194106359-633855201.png" alt="" loading="lazy"></p>
</li>
<li>
<p>4.从上往下开始执行全局代码,依次对GO对象中的全局变量进行赋值。</p>
<ul>
<li>当执行<code>var name = 'curry'</code>时,就从VO(对应的就是GO)中找到name属性赋值为curry;</li>
<li>接下来执行<code>console.log(message)</code>,就从VO中找到message,注意<strong>此时的message还为undefined</strong>,因为message真正赋值在下一行代码,所以就直接打印undefined(也就是我们经常说的变量作用域提升);</li>
<li>后面就依次进行赋值,执行到<code>var result = num1 + num2</code>,也是从VO中找到num1和num2两个属性的值进行相加,然后赋值给result,result最终就为50;</li>
<li>最后执行到<code>foo()</code>,也就是需要去执行foo函数了,这里的操作是比较特殊的,涉及到<strong>函数执行上下文</strong>,下面来详细了解;</li>
</ul>
<p><img src="https://img2020.cnblogs.com/blog/2506425/202201/2506425-20220110194122295-1192663716.png" alt="" loading="lazy"></p>
</li>
</ul>
<h3 id="4函数执行上下文">4.函数执行上下文</h3>
<blockquote>
<p>在执行全局代码遇到函数如何执行呢?</p>
</blockquote>
<ul>
<li>在执行的过程中遇到函数,就会根据函数体创建一个<strong>函数执行上下文(Functional Execution Context,简称FEC)</strong>,并且加入到执行上下文栈(ECS)中。</li>
<li>函数执行上下文(FEC)包含三部分内容:
<ul>
<li>AO:在解析函数时,会创建一个<strong>Activation Objec(AO)</strong>;</li>
<li>作用域链:由<strong>函数VO和父级VO组成</strong>,查找是一层层往外层查找;</li>
<li>this指向:this绑定的值,在函数执行时确定;</li>
</ul>
</li>
<li>其实全局执行上下文(GEC)也有自己的作用域链和this指向,只是它对应的作用域链就是自己本身,而this指向为window。</li>
</ul>
<p>继续来看上面的代码执行,当执行到<code>foo()</code>时:</p>
<ul>
<li>先找到foo函数的存储地址,然后<strong>解析foo函数</strong>,生成函数的AO;</li>
<li>根据AO生成函数执行上下文(FEC),并将其放入执行上下文栈(ECS)中;</li>
<li>开始执行foo函数内代码,依次找到AO中的属性并赋值,当执行<code>console.log(name)</code>时,就会去foo的VO(对应的就是foo函数的AO)中找到name属性值并打印;</li>
</ul>
<p><img src="https://img2020.cnblogs.com/blog/2506425/202201/2506425-20220110194142752-711181739.png" alt="" loading="lazy"></p>
<h3 id="5变量环境和记录">5.变量环境和记录</h3>
<blockquote>
<p>上文中提到了很多次VO,那么VO到底是什么呢?下面从ECMA新旧版本规范中来谈谈VO。</p>
</blockquote>
<p>在早期ECMA的版本规范中:每一个执行上下文会被关联到一个<strong>变量环境(Variable Object,简称VO)</strong>,在源代码中的<strong>变量和函数声明</strong>会被作为属性添加到VO中。对应函数来说,参数也会被添加到VO中。</p>
<ul>
<li>也就是上面所创建的GO或者AO都会被关联到变量环境(VO)上,可以通过VO查找到需要的属性;</li>
<li>规定了VO为Object类型,上文所提到的GO和AO都是Object类型;</li>
</ul>
<p>在最新ECMA的版本规范中:每一个执行上下文会关联到一个<strong>变量环境(Variable Environment,简称VE)</strong>,在执行代码中<strong>变量和函数的声明</strong>会作为<strong>环境记录(Environment Record)</strong>添加到变量环境中。对于函数来说,参数也会被作为环境记录添加到变量环境中。</p>
<ul>
<li>也就是相比于早期的版本规范,对于变量环境,已经去除了VO这个概念,提出了一个新的概念VE;</li>
<li>没有规定VE必须为Object,不同的JS引擎可以使用不同的类型,作为一条环境记录添加进去即可;</li>
<li>虽然新版本规范将变量环境改成了VE,但是JavaScript的执行过程还是不变的,只是关联的变量环境不同,将VE看成VO即可;</li>
</ul>
<h3 id="6全局代码执行过程函数嵌套">6.全局代码执行过程(函数嵌套)</h3>
<blockquote>
<p>了解了上面相关的概念和调用流程之后,就来看一下存在函数嵌套调用的代码是如何执行的,以及执行过程中的一些细节,以下面代码为例:</p>
</blockquote>
<pre><code class="language-js">var message = 'global'
function foo(m) {
var message = 'foo'
console.log(m)
function bar() {
console.log(message)
}
bar()
}
foo(30)
</code></pre>
<ul>
<li>
<p>初始化全局对象(GO),执行全局代码前创建GEC,并将GO关联到VO,然后将GEC加入ECS中:</p>
<ul>
<li>foo函数存储空间中指定的父级作用域为全局对象;</li>
</ul>
<p><img src="https://img2020.cnblogs.com/blog/2506425/202201/2506425-20220110194207014-106982761.png" alt="" loading="lazy"></p>
</li>
<li>
<p>开始执行全局代码,从上往下依次给全局属性赋值:</p>
<ul>
<li>给message属性赋值为global;</li>
</ul>
<p><img src="https://img2020.cnblogs.com/blog/2506425/202201/2506425-20220110194221540-1507559077.png" alt="" loading="lazy"></p>
</li>
<li>
<p>执行到foo函数调用,准备执行foo函数前,创建foo函数的AO:</p>
<ul>
<li>bar函数存储空间中指定父级作用域为foo函数的AO;</li>
</ul>
<p><img src="https://img2020.cnblogs.com/blog/2506425/202201/2506425-20220110194242347-352499302.png" alt="" loading="lazy"></p>
</li>
<li>
<p>创建foo函数的FEC,并加入到ECS中,然后开始执行foo函数体内的代码:</p>
<ul>
<li>根据foo函数调用的传参,给形参m赋值为30,接着给message属性赋值为foo;</li>
<li>所以,m打印结果为30;</li>
</ul>
<p><img src="https://img2020.cnblogs.com/blog/2506425/202201/2506425-20220110194302315-478466182.png" alt="" loading="lazy"></p>
</li>
<li>
<p>执行到bar函数调用,准备执行bar函数前,创建bar函数的AO:</p>
<ul>
<li>bar函数中没有定义属性和声明函数,以空对象表示;</li>
</ul>
<p><img src="https://img2020.cnblogs.com/blog/2506425/202201/2506425-20220110194317995-474847585.png" alt="" loading="lazy"></p>
</li>
<li>
<p>创建bar函数的FEC,并加入到ECS中,然后开始执行bar函数体内的代码:</p>
<ul>
<li>执行<code>console.log(message)</code>,会先去bar函数自己的VO中找message,没有找到就往上层作用域的VO中找;</li>
<li>这里bar函数的父级作用域为foo函数,所以找到foo函数VO中的message为foo,<strong>打印结果为foo</strong>;</li>
</ul>
<p><img src="https://img2020.cnblogs.com/blog/2506425/202201/2506425-20220110194335866-529346625.png" alt="" loading="lazy"></p>
</li>
<li>
<p>全局中所有代码执行完成,bar函数执行上下文出栈,bar函数AO对象失去了引用,进行销毁。</p>
</li>
<li>
<p>接着foo函数执行上下文出栈,foo函数AO对象失去了引用,进行销毁,同样,foo函数AO对象销毁后,bar函数的存储空间也失去引用,进行销毁。</p>
</li>
</ul>
<h3 id="总结">总结:</h3>
<ul>
<li>
<p>函数在执行前就已经确定了其父级作用域,与函数在哪执行没有关系,以函数声明的位置为主;</p>
</li>
<li>
<p>执行代码查找变量属性时,会沿着<strong>作用域链</strong>一层层往上查找(沿着VO往上找),如果一直找到全局对象中还没有该变量属性,就会报错未定义;</p>
</li>
<li>
<p>上文中提到了很多概念名词,下面来总结一下:</p>
<table>
<thead>
<tr>
<th>名词</th>
<th>解释</th>
</tr>
</thead>
<tbody>
<tr>
<td>ECS</td>
<td>执行上下文栈(Execution Context Stack),也可称为调用栈,以栈的形式调用创建的执行上下文</td>
</tr>
<tr>
<td>GEC</td>
<td>全局执行上下文(Global Execution Context),在执行全局代码前创建</td>
</tr>
<tr>
<td>FEC</td>
<td>函数执行上下文(Functional Execution Context),在执行函数前创建</td>
</tr>
<tr>
<td>VO</td>
<td>Variable Object,早期ECMA规范中的变量环境,对应Object</td>
</tr>
<tr>
<td>VE</td>
<td>Variable Environment,最新ECMA规范中的变量环境,对应环境记录</td>
</tr>
<tr>
<td>GO</td>
<td>全局对象(Global Object),解析全局代码时创建,GEC中关联的VO就是GO</td>
</tr>
<tr>
<td>AO</td>
<td>函数对象(Activation Object),解析函数体代码时创建,FEC中关联的VO就是AO</td>
</tr>
</tbody>
</table>
</li>
</ul><br><br>
来源:https://www.cnblogs.com/MomentYY/p/15785719.html
頁:
[1]