注册

V8系列第二篇:从执行上下文的角度看JavaScript到底是怎么运行的


1.前言


先来说一说V8引擎和浏览器


V8引擎要想运行起来,就必须依附于浏览器,或者依附于Node.js宿主环境。因此V8引擎是被浏览器或者Node.js启动的。比如在Chrome浏览器中,你打开一个网址后,渲染进程便会初始化V8引擎,同时在V8中会初始化堆空间和栈空间,而栈空间就是来管理执行上下文的。而执行上下文就可以说是我们平常写的JavaScript代码的运行环境。


好了简单理解一下,那接下来本篇就重点来学习一下V8引擎中的执行上下文


2.执行上下文概述



首先从宏观的角度来说: JavaScript代码要想能够被执行,就必须先被V8引擎编译,编译完成之后才会进入到执行阶段,总结为六个字:先编译再执行



在V8引擎编译的过程中,同时会生成执行上下文。最开始执行代码的时候通常会生成全局执行上下文、执行一个函数时会生成该函数的执行上下文、当执行一个代码块时也会生成代码块的可执行上下文。所以一段代码可以说成是先编译再执行,那么整个过程就是无数个先编译再执行构成的(通常编译发生在执行代码前的几微秒,甚至更短的时间)。


我们再来理解一下上面说到的执行上下文, 在JavaScript 高级程序设计(第四版)中大概是这样描述的:



执行上下文的概念在JavaScript中是非常重要的。变量或者函数的执行上下文决定了它们可以访问哪些数据,以及他们拥有哪些行为(可以执行哪些方法吧)。每个执行上下文都有一个关联的变量对象,而这个执行上下文中定义的所有变量和函数都存在于这个对象上。这个对象我们在代码中是无法访问的。



执行上下文可以说是执行一段JavaScript代码的运行环境,可以算作是一个抽象的概念。


简单的理解一下概念(下文如果再需要的时候你可以返回顶部再次理解查看)后,我们就来看看JavaScript是怎么将一个变量和函数运行起来的。


3.准备测试代码


这里为了更直观的查看代码的运行效果,我特意新建了一个xxxx.html文件,文件所有代码如下所示:
这里突然发现html文件中只有script标签和js代码也是可以执行的,不清楚以前是不是也是可以,还是说JavaScript引擎在后期做了优化处理。


<script>
a_Function()
var a_variable = 'aehyok'
console.log(a_variable)
function a_Function() {
console.log('函数a_Function执行了', a_variable);
}
</script>


特别强调一个点,我上面声明变量使用的var关键字



运行后的执行结果


image.png


4.调试var声明的变量


相信通过运行结果,你心中应该有了自己的代码执行过程了。我们接着往下操作,在第2行代码(下文截图中的位置)打个调试断点,如下图所示


image.png


此时代码已经准备开始要执行a_Function函数了。脑补一下,我们就以此为分割点(按正常来说这肯定是不合理的,因为代码已经开始执行了,不过你可以暂且这样尝试去理解一下),就是运行到第2行代码之前的时间段或者状态,我们就称它为编译阶段,这之后代码就开始运行了,我们称它为执行阶段


1、通过截图可以发现,作用域下的全局 已经有了一个a_Function函数,以及一个a_variable变量其值为 undefined,这里可以看到许许多多的其他变量、函数,这其实就是全局window对象。


2、使用过JavaScript的人都清楚,JavaScript是按照顺序执行代码的,但是通过截图去看,好像又不太对劲,所以执行前的编译阶段,JavaScript引擎还是处理了不少事情的,它做了什么事情呢?


V8引擎编译这段代码的时候,同时会生成一个全局执行上下文,在截图的第二行代码发现是一个函数,便会在代码中查找到该函数的定义,并将该函数体放到全局执行上下文词法环境中。该函数体里的代码还未执行,所以不会去编译,继续第三行代码,发现是var声明的一个变量,便会将该变量放到全局执行上文变量环境中,同时给该变量赋值为undefined。


具体如下模拟代码


function a_Function() {
console.log('函数a_Function执行了', a_variable);
}
var a_variable = undefined

这段代码主要在编译代码阶段做了变量提升,会将var声明的变量存放到变量环境中(let和const声明的变量存放到词法环境中),而函数的声明会被存放到词法环境中。
词法环境变量环境是存在于执行上下文的,变量的默认值会被设置为undefined,函数的执行体会被带到词法环境
然后还会生成可执行代码,其实编译生成的是字节码,下面的代码算是模拟代码:


a_Function()
a_variable = 'aehyok'
console.log(a_variable)

  • 执行阶段
    接下来开始按照顺序执行上面生成的可执行代码,其实在执行阶段已经变成了机器码

a_Function()
a_variable = 'aehyok'
console.log(a_variable)

第一行模拟代码:先调用a_Function,此时会开始生成该函数的函数执行上下文, 执行a_Function中的代码,函数a_Function执行了 undefined,因为此时的a_variable还没给予赋值操作


第二行模拟代码:对a_variable变量进行赋值字符串"aehyok",此时变量环境中的a_variable值变为"aehyok"


第三行模拟代码:打印已经赋值为aehyok的变量。


5.调试let声明的变量


5.1主要是将上面的测试代码中:声明变量的关键字var改为let


<script>
a_Function()
let a_variable = 'aehyok'
console.log(a_variable)
function a_Function() {
console.log('函数a_Function执行了', a_variable);
}
</script>

执行代码以后发现直接报错了,报错内容如下图所示


image.png


5.2打断点调试代码


image.png
代码断点打到如截图中第2行位置,可以看到let声明的变量,存在于单独的Script作用域中,并且赋值为undefined。


5.3分析5.1和5.2的代码


  • 通过varlet两种方式代码运行比对情况来看,let声明变量的方式不存在变量提升的情况。
  • 通过3.2截图可以发现,let声明变量的方式,在作用域中的已经创建,并赋值为undefined,但通过查阅资料发现:


let声明的变量,主要是因为V8虚拟机做了限制,虽然a_variable已经在内存中并且赋值为undefined,但是当你在let a_variable 之前访问a_variable时,根据ECMAScript定义,虚拟机会阻止的访问!也可以说成是形成了暂时性的死区,这是语法规定出来的。所以就会报错。



6.调试let声明的变量继续执行


主要添加了一个let声明的变量,以及为其进行了赋值操作,代码如下所示


<script>
a_Function()
var a_variable = 'a_aehyok'
let aa_variable = 'aa_aehyok'
console.log(a_variable)
function a_Function() {
console.log('函数a_Function执行了', a_variable);
}
</script>

执行后情况截图如下


image.png


可以发现通过var声明的变量和let(也可以使用const)声明的变量被储存在了不同的位置,之前上面说过通过var声明的变量被存放到了变量环境中了。那么现在我再告诉你,通过let(也可以是const)声明的变量被存放到了词法环境中了。


  • var声明的变量存放在变量环境
  • let和const声明的变量存放在词法环境
  • 函数的声明存放在词法环境
  • 变量环境词法环境都存在于执行上下文

7.总结三种执行上下文


在上面的一小段代码中,我们已经使用过了两种执行上下文,全局执行上下文函数执行上下文


  • 全局执行上下文 — 这是默认或者说基础的上下文,任何不在函数内部的代码都在全局上下文中。它会执行两件事:创建一个全局的 window 对象(浏览器的情况下),并且设置 this 的值等于这个全局对象。一个程序中只会有一个全局执行上下文。


var声明的变量会在全局window对象上,而let和const声明的变量是不会在全局window对象上的。而全局函数时会在全局window对象上。




  • 函数执行上下文 — 每当一个函数被调用时, 都会为该函数创建一个新的上下文。每个函数都有它自己的执行上下文,不过是在函数被调用时创建的。函数上下文可以有任意多个。每当一个新的执行上下文被创建,它会按定义的顺序(将在后文讨论)执行一系列步骤。



  • Eval 函数执行上下文 — 执行在 eval 函数内部的代码也会有它属于自己的执行上下文,但由于 JavaScript 开发者并不经常使用 eval,所以在这里我不会讨论它。



8.总结



  • 1、通过这篇简单的文章,我想我自己理清楚了,原来JavaScript代码是先编译再执行的。



  • 2、然后代码在编译的时候就生成了执行上下文,也就是代码运行的环境。



  • 3、var声明的变量存在变量提升,并且在编译阶段存放到了变量环境中,变量环境其实也是一个词法环境



  • 4、通过变量提升发现,代码会先生成执行上下文,然后再生成可执行的代码



  • 5、const和let声明的变量不存在变量提升,并且再编译阶段被存放到了词法环境中。



  • 6、所有var定义的全局变量和全局定义的函数,都会在window对象上。



  • 7、所有let和const定义的全局变量不会定义在全局上下文中,但是在作用域链的解析效果上是一样的(跟var定义的)。

 

0 个评论

要回复文章请先登录注册