上个月写过一篇V8是如何运行JavaScript(let a = 1)代码的?,写完之后我就发现,我对平常使用的工具V8引擎,偏底层的知识了解的竟然是如此甚少。同时我真正从事前端的时间还算是比较短的,那么基础也算是非常的薄弱。结合以上,我打算有时间就去从底层的角度去学习了解,便于在使用过程中的理解和解决遇到的问题,理解JavaScript的本质,能够更好的学习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关键字
运行后的执行结果
4.调试var声明的变量
相信通过运行结果,你心中应该有了自己的代码执行过程了。我们接着往下操作,在第2行代码
(下文截图中的位置)打个调试断点,如下图所示
此时代码已经准备开始要执行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>
执行代码以后发现直接报错了,报错内容如下图所示
5.2打断点调试代码
代码断点打到如截图中第2行位置
,可以看到let
声明的变量,存在于单独的Script作用域
中,并且赋值为undefined。
5.3分析5.1和5.2的代码
- 通过
var
和let
两种方式代码运行比对情况来看,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>
执行后情况截图如下
可以发现通过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定义的)。