前言
我们天天写业务,公司小,业务简单,经验不值钱,出来面试被说没亮点,写个业务都要问原理,如果没答好,就开始以下场景:
面试官:“你有什么要问我的吗?”
你:“额,我,我没什么要问的。。。”
不,其实你不是这样子的,你不想这么憋屈的离开,你不想就这样辛辛苦苦跑过来被两三句话打发掉!
所以,开启绝地反击模式:
面试官:“你有什么要问我的吗?”
你:“js的运行时是怎么样的?”
面试官:“我。。。”
运行时解析
如下代码
var a = 1
let b = 2
const c = {k: 3}
function d () {
console.log(a)
var a = 11
let b = 22
dd()
}
function dd () {
console.log(b)
}
d() // 输出啥? undefined 2
如果你是js老手, 一看d()执行结果就知道是a变量提升了打印undefined,dd方法执行打印2,那a为什么会提升呢, 怎么不是打印外面的变量a = 1呢?b怎么不打印22呢?想要弄清楚这些问题,就要了解js的执行机制了。
js运行时有两个阶段:编译阶段、执行阶段。
编译阶段:js通过编译生成执行上下文和可执行代码两部分。
执行阶段:执行可执行代码,输出结果。
可以理解如下图
执行上下文是啥?可执行代码又是啥?
执行上下文是 JavaScript 执行一段代码时的运行环境,比如调用一个函数,就会进入这个函数的执行上下文,确定该函数在执行期间用到的诸如 this、变量、对象以及函数等。
可执行代码就是指js引擎可以执行的代码,这些代码可以是【字节码、机器码】。我们写的js代码它看不懂,也不会执行,它要先把js编译成字节码、机器码才行。
其实不管是什么高级语言代码都要编译才能执行,后面再说。
了解了这两个概念还不够,执行上下文里面有哪些内容?可以如下图表示
看上图,这是编译阶段的输出,可以了解到一些重要信息:
变量环境:执行上下文中var 声明的变量,且赋值默认值undefined。
词法环境:执行上下文中let const声明的变量,这是解决变量提升问题,重点这是在es6加入的,es5并没有词法环境。
outer: 外部引用,其实在每个执行上下文中,都包含了一个外部引用,用来指向外部的执行上下文,我们把这个外部引用称为 outer。在js中有全局执行上下文、函数上下文之说,每个上下文的外部引用都是在编译时决定的,这个和代码的编译时的位置有关,也称为词法作用域。词法作用域就是指作用域是由代码中函数声明的位置来决定的,所以词法作用域是静态的作用域,通过它就能够预测代码在执行过程中如何查找标识符。理解可以参考下图
this: this是一套和作用域无关的独立机制,这个你可以另外找到相关文章深入理解。
变量提升其实就是函数运行的编译阶段,把所有var声明的变量统一提升到该作用域“顶部”, 并赋默认值undefined。
所以执行d()函数的时候,相当于以下流程,先编译再执行
var a = undefined
console.log(a)
a = 11
而dd()执行的时候其实就是按照上面说的,由于dd上下文没有声明b变量,根据作用域链查找外部引用,直到首次找到b,而作用域在编译阶段就确定了外部引用。具体可以参考下图:
所以,就例子而言无论d方法还是dd方法,它们的外部引用都是指向全局上下文,在执行阶段,如果发现函数作用域没有变量声明就会沿着作用域往外部引用查找。
总结一下
1、js运行时有两个阶段:编译阶段 和 执行阶段
2、变量提升的根本原因是js在编译阶段确定的
3、执行上下文有变量环境与词法环境,变量环境存储var声明的变量,词法环境存储let cosnt声明的变量。
4、每个作用域的外部引用在编译阶段根据代码的位置确定。
v8引擎角度看javascript运行时
上面说到js运行时有编译阶段 和 执行阶段两个阶段,那么再从js引擎的角度来看,这是怎么一回事。
首先先了解一些概念:
编译器和解释器
之所以存在编译器和解释器,是因为机器不能直接理解我们所写的代码,所以在执行程序之前,需要将我们所写的代码“翻译”成机器能读懂的机器语言。
按语言的执行流程,可以把语言划分为编译型语言和解释型语言。编译型语言在程序执行之前,需要经过编译器的编译过程,并且编译之后会直接保留机器能读懂的二进制文件,这样每次运行程序时,都可以直接运行该二进制文件,而不需要再次重新编译了。比如 C/C++、GO 等都是编译型语言。而由解释型语言编写的程序,在每次运行时都需要通过解释器对程序进行动态解释和执行。比如 Python、JavaScript 等都属于解释型语言。
了解上面的概念之后,v8引擎就是走的基于解释器的路线,但是又与它不同,是一个改进版。具体如下图
从图中可以看出v8是解释器、编译器一起用了的。
通常,如果有一段第一次执行的字节码,解释器 Ignition 会逐条解释执行。解释器 Ignition 除了负责生成字节码之外,它还有另外一个作用,就是解释执行字节码。在 Ignition 执行字节码的过程中,如果发现有热点代码(HotSpot),比如一段代码被重复执行多次,这种就称为热点代码,那么后台的编译器 TurboFan 就会把该段热点的字节码编译为高效的机器码,然后当再次执行这段被优化的代码时,只需要执行编译后的机器码就可以了,这样就大大提升了代码的执行效率。
回到上面的js运行时两个阶段,编译阶段和执行阶段,编译阶段就是源码 ->AST -> 字节码。执行阶段就是基于字节码、机器码执行。
最后
好了,以上就是javascript运行时的内容,了解这一过程希望对你开发过程有所帮助。