重学JavaScript02----- 深入JavaScript运行原理
文章目录
前言
JavaScript使用var关键字声明变量为什么会存在变量提升?
JavaScript函数为什么可以提前调用?
JavaScript代码,在浏览器中又是如何被执行的?
我们可以通过了解V8引擎的运行机制了解JS代码在浏览器中的编译过程,来解释为什么存在声明提升的情况
V8引擎的解析图(官方)
V8执行的细节
那么我们的JavaScript源码是如何被解析(Parse过程)的呢?
-
Blink将源码交给V8引擎,Stream获取到源码并且进行编码转换;
-
Scanner会进行词法分析(lexical analysis),词法分析会将代码转换成tokens;
-
接下来tokens会被转换成AST树,经过Parser和PreParser:
-
Parser就是直接将tokens转成AST树架构;
PreParser称之为预解析,为什么需要预解析呢?
重点:预解析是导致JavaScript代码变量提升的关键
这是因为并不是所有的JavaScript代码,在一开始时就会被执行。那么对所有的JavaScript代码进行解析,必然会影响网页的运行效率;
所以V8引擎就实现了Lazy Parsing
(延迟解析)的方案,它的作用是将不必要的函数进行预解析,也就是只解析暂
时需要的内容,而对函数的全量解析是在函数被调用时才会进行;
比如我们在一个函数outer内部定义了另外一个函数inner,那么inner函数就会进行预解析;
它不会解析inner中的代码;只有在调用inner后才会解析。以此提高浏览器性能;
生成AST树后,会被Ignition转成字节码(bytecode),之后的过程就是代码的执行过程(后续会详细分析)
初始化全局对象
js引擎会在执行代码之前,会在堆内存中创建一个全局对象:Global Object(GO)
该对象 所有的作用域(scope)都可以访问;
里面会包含Date
、Array
、String
、Number
、setTimeout
、setInterval
等等;
其中还有一个window属性指向自己;
node环境: 是global而不是window
执行上下文栈(调用栈)
js引擎内部有一个执行上下文栈(Execution Context Stack,简称ECS),它是用于执行代码的调用栈。
那么现在它要执行谁呢?执行的是全局的代码块:
- 全局的代码块为了执行会构建一个
Global Execution Context(GEC)
; GEC
会 被放入到ECS
中 执行;
GEC被放入到ECS中里面包含两部分内容:
第一部分:在代码执行前,在parser转成AST的过程中,会将全局定义的变量、函数等加入到GlobalObject中,
但是并不会赋值;
这个过程也称之为变量的作用域提升(hoisting
)
第二部分:在代码执行中,对变量赋值,或者执行其他的函数;
JavaScript的执行过程
变量提升的原因
假如我们有下面一段代码,它在JavaScript中是如何被执行的呢?
执行过程1:预解析
执行过程2: 开始执行依次往下执行代码
第一行的name变成 => ‘八嘎’
第三行的foo函数内的name因为没调用 name =‘undefined’
第八行打印GO预解析中的num1 :undefined
第十四行调用foo函数 往下执行name从undefined 被覆盖为‘压路’
第五行打印‘压路
第十五行打印1346
打印num
函数如何执行?
在执行的过程中执行到一个函数时,就会根据函数体创建一个函数执行上下文(Functional Execution Context
,
简称FEC
),并且压入到EC Stack
中。
FEC中包含三部分内容:
- 在解析函数成为AST树结构时,会创建一个Activation Object(AO):
AO中包含形参、arguments、函数定义和指向函数对象、定义的变量; - 作用域链:由VO(在函数中就是AO对象)和父级VO组成,查找时会一层层查找;
- this绑定的值:根据不同情况绑定this;
function foo() {
console.log(n)
var n = 200
console.log(n)
}
var n = 100
foo()
执行过程:预解析
直到foo()调用函数
根据函数体创建一个函数执行上下文,压入ECS
继续从上往下执行代码
我们打印第一个console.log(n)时会发现是n 是 undefined
因为在解析函数成为AST树结构时,会创建一个Activation Object(AO),而AO中不会管代码怎么上下执行,foo函数内var定义的a直接被定义为了undefined,根据作用域链的规则,我们会首先打印内部的n,所以打印才是undefined,而不是100
继续执行下一句代码 var n=200 AO中的 n:undefined 被覆盖为200
打印 n 既得 200