javaScript是什么?
javaScript是一门高级的编程语言。而我们从编程语言发展历史来说,可以划分为三个阶段:
- 机器语言:1000100111011000,一些机器指令;
- 汇编语言:mov ax,bx,一些汇编指令;
- 高级语言:C、C++、Java、JavaScript、Python;
高级语言:
- 编译型语言:将代码编译成可执行文件,直接放在电脑里面进行执行。
- C、C++
- Java目前还存在争议。
- 解析型语言:一边读代码一边进行解释。
- JavaScript、Python
转化
对于计算机本身来说,它并不认识这些高级语言,所以我们的代码最终还是需要被转化为机器指令。
浏览器工作原理
- 当我们在浏览器输入域名的时候,会通过
DNS域名解析,解析成为一个ip地址,这个ip地址就是服务器的地址,然后当我们敲回车的时候,服务器就会给我们返回一个index.html。 - 在浏览器对html解析的过程中,比如遇到了
link引入的CSS文件,那么就会将CSS文件下载下来,然后继续进行解析,如果遇到js文件,那么同样的道理,也会把js文件下载下来。
那么这些代码是如何没浏览器进行解析的呢?那么就要提出另外一个概念,叫做浏览器内核。
浏览器内核
我们可能会听过这样一句话:不同的浏览器是由不同的内核组成的。
常见的浏览器内核
- Gecko:早期被Netscape和Mozilla Firefox浏览器浏览器使用;
- Trident:微软开发,被IE4~IE11浏览器使用,但是Edge浏览器已经转向Blink;
- Webkit:苹果基于KHTML开发、开源的,用于Safari,Google Chrome之前也在使用;
- Blink:是Webkit的一个分支,Google开发,目前应用于Google Chrome、Edge、Opera等;
- 等等.....
我们经常说的浏览器内核指的是浏览器的排版引擎:
排版引擎(layout engine),也称为浏览器引擎(browser engine)、页面渲染引擎(rendering engine) 或样版引擎。
认识JS引擎
为什么需要JS引擎
- 我们前面说过,高级的编程语言都是需要转成最终的机器指令来执行的;
- 事实上我们编写的JavaScript无论你交给浏览器或者Node执行,最后都是需要被CPU执行的;
- 但是CPU只认识自己的指令集,实际上是机器语言,才能被CPU所执行;
- 所以我们需要JavaScript引擎帮助我们将JavaScript代码翻译成CPU指令来执行;
常见的JS引擎
- SpiderMonkey:第一款JavaScript引擎,由Brendan Eich开发(也就是JavaScript作者);
- Chakra:微软开发,用于IT浏览器;
- JavaScriptCore:WebKit中的JavaScript引擎,Apple公司开发;
- V8:Google开发的强大JavaScript引擎,也帮助Chrome从众多浏览器中脱颖而出;
- 等等.......
浏览器内核和JS引擎的关系
这里我们以WebKit为例,WebKit事实上由两部分组成的:

- WebCore:负责HTML解析、布局、渲染等等相关的工作;
- JavaScriptCore:解析、执行JavaScript代码;
在小程序中编写的JavaScript代码就是被JSCore执行的;
V8引擎的原理
定义
官方:V8是用C ++编写的Google开源高性能JavaScript和WebAssembly(未来js的替代产品)引擎,它用于Chrome和Node.js等。
它实现ECMAScript和WebAssembly,并在Windows 7或更高版本,macOS 10.12+和使用x64,IA-32, ARM或MIPS处理器的Linux系统上运行。
V8可以独立运行,也可以嵌入到任何C ++应用程序中。

V8引擎的架构
V8引擎本身的源码非常复杂,大概有超过100w行C++代码,通过了解它的架构,我们可以知道它是如何对JavaScript执行的:
- Parse模块会将JavaScript代码转换成AST(抽象语法树),这是因为解释器并不直接认识JavaScript代码;
- 如果函数没有被调用,那么是不会被转换成AST的;
- Ignition是一个解释器,会将AST转换成ByteCode(字节码)
- 同时会收集TurboFan优化所需要的信息(比如函数参数的类型信息,有了类型才能进行真实的运算);
- 如果函数只调用一次,Ignition会执行解释执行ByteCode;
- TurboFan是一个编译器,可以将字节码编译为CPU可以直接执行的机器码;
- 如果一个函数被多次调用,那么就会被标记为热点函数,那么就会经过TurboFan转换成优化的机器码,提高代码的执行性能;
- 但是,机器码实际上也会被还原为ByteCode,这是因为如果后续执行函数的过程中,类型发生了变化(比如sum函数原来执行的是 number类型,后来执行变成了string类型),之前优化的机器码并不能正确的处理运算,就会逆向的转换成字节码;
V8的细节

那么我们的JavaScript源码是如何被解析(Parse)过程的呢?
- Blink将源码交给V8引擎,Stream获取到源码并且进行编码转换;
- Scanner会进行词法分析(lexical analysis),词法分析会将代码转换成tokens;
- 接下来tokens会被转换成AST树,经过Parser和PreParser
- Parser就是直接将tokens转成AST树架构;
- PreParser称之为预解析,为什么需要预解析呢?
- 这是因为并不是所有的JavaScript代码,在一开始时就会被执行。那么对所有的JavaScript代码进行解析,必然会影响网页的运行效率;
- 所以V8引擎就实现了Lazy Parsing(延迟解析)的方案,它的作用是将不必要的函数进行预解析,也就是只解析暂 时需要的内容,而对函数的全量解析是在函数被调用时才会进行;
- 比如我们在一个函数outer内部定义了另外一个函数inner,那么inner函数就会进行预解析;
- 生成AST树后,会被Ignition转成字节码(bytecode),之后的过程就是代码的执行过程
初始化全局对象
执行过程
var name = "cs";
function foo() {
var name = "foo";
console.log(name);
}
var n1 = 20;
var n2 = 30;
var res = n1 + n2;
console.log(res);
foo()
假如我们有上面一段代码,它在JavaScript中是如何被执行的呢?
js引擎会在执行代码之前,会在堆内存中创建一个全局对象:Global Object(GO)
- 该对象所有的作用域(scope)都可以访问,就相当于是window对象;
- 里面会包含Date、Array、String、Number、setTimeout、setInterval等等;
- 其中还有一个window属性指向自己;
执行上下文
js引擎内部有一个执行上下文栈(Execution Context Stack,简称ECS),它是用于执行代码的调用栈。
那么现在它要执行谁呢?执行的是全局的代码块:
- 全局的代码块为了执行会构建一个 Global Execution Context(GEC);
- GEC会 被放入到ECS中 执行;
GEC被放入到ECS中里面包含两部分内容:
- 在代码执行前,在parser转成AST的过程中,会将全局定义的变量、函数等加入到GlobalObject中,但是并不会赋值;这个过程也称之为变量的作用域提升(hoisting)。
- 在代码执行中,对变量赋值,或者执行其他的函数;
认识VO对象:
- 每一个执行上下文会关联一个VO(Variable Object,变量对象),变量和函数声明会被添加到这个VO对象中。
- 当全局代码被执行的时候,VO就是GO(关联一个VO)对象了
全局代码执行过程


函数的执行
在执行的过程中执行到一个函数时,就会根据函数体创建一个函数执行上下文(Functional Execution Context,简称FEC),并且压入到EC Stack中。
因为每个执行上下文都会关联一个VO,那么函数执行上下文关联的VO是什么呢?
- 当进入一个函数执行上下文时(函数没有执行的时候,不会创建AO),会创建一个AO对象(Activation Object);
- 这个AO对象会使用arguments作为初始化,并且初始值是传入的参数;
- 这个AO对象会作为执行上下文的VO来存放变量的初始化;
- 当执行完成之后,会销毁(弹出栈)掉;
- 当重复调用函数的时候,会创建一个新的AO(和原来那个毫无关系),原来那个大概率会被销毁掉(不排除闭包)
函数的执行过程


作用域和作用域链
当进入到一个执行上下文时,执行上下文也会关联一个作用域链(Scope Chain)。
- 作用域链是一个对象列表,用于变量标识符的求值;
- 当进入一个执行上下文时,这个作用域链被创建,并且根据代码类型,添加一系列的对象;
- 作用域链是在全局代码被解析时候,就已经创建好了。和调用的位置没有关系。
var message = "global message";
function foo(){
console.log(message);//global message
}
function bar(){
var message = "bar message";
foo();
}
bar()
当查找变量的时候,优先在自己的VO里面进行查找。