JS的解释阶段
首先得明确: JS是解释型语言,所以它无需提前编译,而是由解释器实时运行
引擎对JS的处理过程可以简述如下:
读取代码,进行词法分析(Lexical analysis),然后将代码分解成词元(token)
对词元进行语法分析(parsing),然后将代码整理成语法树(syntax tree)
使用翻译器(translator),将代码转为字节码(bytecode)
使用字节码解释器(bytecode interpreter),将字节码转为机器码
最终计算机执行的就是机器码。
为了提高运行速度,现代浏览器一般采用即时编译(JIT-Just In Time compiler)
即字节码只在运行时编译,用到哪一行就编译哪一行,并且把编译结果缓存(inline cache)
这样整个程序的运行速度能得到显著提升。
而且,不同浏览器策略可能还不同,有的浏览器就省略了字节码的翻译步骤,直接转为机器码(如chrome的v8)
总结起来可以认为是: 核心的JIT编译器将源码编译成机器码运行
JS预处理阶段
上述讲的是解释器的整体过程,这里再提下在正式执行JS前,还会有一个预处理阶段
(譬如变量提升,分号补全等)
预处理阶段会做一些事情,确保JS可以正确执行,这里仅提部分:
分号补全
JS执行是需要分号的,但为什么以下语句却可以正常运行呢?
console.log('a')
console.log('b')
原因就是JS解释器有一个Semicolon Insertion规则,它会按照一定规则,在适当的位置补充分号
譬如列举几条自动加分号的规则:
当有换行符(包括含有换行符的多行注释),并且下一个token没法跟前面的语法匹配时,会自动补分号。
当有}时,如果缺少分号,会补分号。
程序源代码结束时,如果缺少分号,会补分号。
于是,上述的代码就变成了
console.log('a');
console.log('b');
所以可以正常运行
当然了,这里有一个经典的例子:
function b() {
return
{
a: 'a'
};
}
由于分号补全机制,所以它变成了:
function b() {
return;
{
a: 'a'
};
}
所以运行后是undefined
推荐阅读:
变量提升
一般包括函数提升和变量提升
譬如:
a = 1;
b();
function b() {
console.log('b');
}
var a;
经过变量提升后,就变成:
function b() {
console.log('b');
}
var a;
a = 1;
b();
这里没有展开,其实展开也可以牵涉到很多内容的
譬如可以提下变量声明,函数声明,形参,实参的优先级顺序,以及es6中let有关的临时死区等
JS的执行阶段
此阶段的内容中的图片来源:深入理解JavaScript系列(10):JavaScript核心(晋级高手必读篇)
解释器解释完语法规则后,就开始执行,然后整个执行流程中大致包含以下概念:
执行上下文,执行堆栈概念(如全局上下文,当前活动上下文)
VO(变量对象)和 AO(活动对象)
作用域链
this机制等
这些概念如果深入讲解的话内容过多,因此这里仅提及部分特性
执行上下文简单解释
JS有执行上下文:
浏览器首次载入脚本,它将创建全局执行上下文,并压入执行栈栈顶(不可被弹出)
然后每进入其它作用域就创建对应的执行上下文并把它压入执行栈的顶部
一旦对应的上下文执行完毕,就从栈顶弹出,并将上下文控制权交给当前的栈。
这样依次执行(最终都会回到全局执行上下文)
譬如,如果程序执行完毕,被弹出执行栈,然后有没有被引用(没有形成闭包),那么这个函数中用到的内存就会被垃圾处理器自动回收
然后执行上下文与VO,作用域链,this的关系是:
每一个执行上下文,都有三个重要属性:
变量对象(Variable object,VO) 作用域链(Scope chain) this
VO与AO:
VO是执行上下文的属性(抽象概念),但是只有全局上下文的变量对象允许通过VO的属性名称来间接访问(因为在全局上下文里,全局对象自身就是变量对象)
AO(activation object),当函数被调用者激活,AO就被创建了
可以理解为:
在函数上下文中:VO === AO
在全局上下文中:VO === this === global
总的来说,VO中会存放一些变量信息(如声明的变量,函数,arguments参数等等)
作用域链:
它是执行上下文中的一个属性,原理和原型链很相似,作用很重要。
譬如流程简述:
在函数上下文中,查找一个变量foo
如果函数的VO中找到了,就直接使用
否则去它的父级作用域链中(parent)找
如果父级中没找到,继续往上找
直到全局上下文中也没找到就报错
this指针:this是执行上下文环境的一个属性,而不是某个变量对象的属性
垃圾回收
引用计数:引用计数方法对循环引用会出现问题
标记清除:从全局根开始遍历所有对象并标记,直到没有子节点和可遍历的路径。垃圾回收器会忽略所有可访问对象,因为在遍历的时候已经被标记。所以未标记的对象就是从根节点无法访问的对象,意味着可以被回收。
自 2012 年以来,JavaScript 引擎已经使用标记清除算法来代替引用计数垃圾回收。
推荐阅读:
[译] 通过垃圾回收机制理解 JavaScript 内存管理
译」Orinoco: V8的垃圾回收器
摘选自:从输入URL到页面加载的过程?如何由一道题完善自己的前端知识体系!
JavaScript 是如何工作的:渲染引擎和性能优化技巧