只要往前多想点,你的故事就能更让人感同身受。
JavaScript编译原理(V8引擎)
几个重要概念和原理:
- 编译器(Compiler)
- 解释器(Interpreter)
- 抽象语法树(AST)
- 字节码(Bytecode)
- 即时编译器(JIT)
编译器和解释器
编译型语言和解释型语言
之所以存在编译器和解释器,是因为机器不能直接理解我们所写的代码,所以在执行程序之前,需要将我们所写的代码“翻译”成机器能读懂的机器语言。按语言的执行流程,可以把语言划分为
编译型语言和解释型语言。
编译型语言在程序执行之前,需要经过编译器的编译过程,并且编译之后会直接保留机器能读懂的二进制文件,这样每次运行程序时,都可以直接运行该二进制文件,而不需要再次重新编译了。比如 C/C++、GO 等都是编译型语言。
解释型语言编写的程序,在每次运行时都需要通过解释器对程序进行动态解释和执行。比如 Python、JavaScript 等都属于解释型语言。
编译器执行过程
编译器首先会依次对源代码进行词法分析、语法分析,生成抽象语法树(AST),然后是优化代码,最后再生成处理器能够理解的机器码。如果编译成功,将会生成一个可执行的文件。但如果编译过程发生了语法或者其他的错误,那么编译器就会抛出异常,最后的二进制文件也不会生成成功。
解释器执行过程
解释器也会对源代码进行词法分析、语法分析,并生成抽象语法树(AST),不过它会再基于抽象语法树生成字节码,最后再根据字节码来执行程序、输出结果。
V8引擎执行JavaScript代码过程
- Ignition:V8引擎的
解释器 - TurboFan:V8引擎的
编译器
- 生成抽象语法树(AST)和执行上下文
将源代码转换为抽象语法树,并生成执行上下文。
生成AST
过程:- 第一阶段是分词(tokenize),又称为词法分析。
- 第二阶段是解析(parse),又称为语法分析。
生成该段代码的执行上下文关于AST树生成及应用
- 生成字节码
- 字节码就是介于 AST 和机器码之间的一种代码。但是与特定类型的机器码无关,字节码需要通过解释器将其转换为机器码后才能执行。
- 执行代码
- 通常,如果有一段第一次执行的字节码,
解释器Ignition会逐条解释执行。在执行字节码的过程中,如果发现有热点代码(HotSpot),比如一段代码被重复执行多次,这种就称为热点代码,那么后台的编译器TurboFan就会把该段热点的字节码编译为高效的机器码,然后当再次执行这段被优化的代码时,只需要执行编译后的机器码就可以了,这样就大大提升了代码的执行效率。 - 关于V8引擎即时编译(JIT):解释器 Ignition 在解释执行字节码的同时,收集代码信息,当它发现某一部分代码变热了之后,TurboFan 编译器便闪亮登场,把热点的字节码转换为机器码,并把转换后的机器码保存起来,以备下次使用。
- 通常,如果有一段第一次执行的字节码,
执行上下文(Execution Context,EC)
作用域
变量或函数的执行上下文决定了它们可以访问哪些数据,以及它们的行为。每个执行上下文都有一个关联的变量对象(variable object), 虽然这个执行上下文中定义的所有变量和函数都存在于这个对象上,但无法通过代码访问变量对象
作用域的分类
根据语言分类
- 词法作用域:也称为静态作用域。(JavaScript采用)
- 动态作用域:相对“冷门”,但确实有一些语言采纳的是动态作用域,如:Bash 脚本、Perl 等 词法作用域和动态作用域的区别其实在于划分作用域的时机:
- 词法作用域: 在代码书写的时候完成划分,作用域链沿着它定义的位置往外延伸
- 动态作用域: 在代码运行时完成划分,作用域链沿着它的调用栈往外延伸。
词法作用域(静态作用域)
- 全局作用域(全局上下文)
- 函数作用域(函数上下文-
函数定义时候就决定了) - 块级作用域(块级上下文)
- 全局上下文是最外层的上下文。根据 ECMAScript 实现的宿主环境,表示全局上下文的对象可能不一 样。在浏览器中(非严格模式下),全局上下文就是我们常说的 window 对象,所有通过 var 定义的全局变量和函数都会成为 window 对象的属性和方法。使用 let 和 const 的顶级声明不会定义在全局上下文中,但在作用域链解析上效果是一样的。上下文在其所有代码都执行完毕后会被销毁,包括定义 在它上面的所有变量和函数(全局上下文在应用程序退出前才会被销毁,比如关闭网页或退出浏览器)。
- 函数上下文,当代码执行流进入函数时,函数的上下文被推到一个上下文栈上。 在函数执行完之后,上下文栈会弹出该函数上下文,将控制权返还给之前的执行上下文。
ECMAScript 程序的执行流就是通过这个上下文栈进行控制的。 - 块级上下文 关联的变量对象(variable object)[[scope]更多]
作用域链
- 上下文中的代码在执行的时候,会创建变量对象的一个作用域链(scope chain)。这个作用域链决定 了各级上下文中的代码在访问变量和函数时的顺序。代码正在执行的上下文的变量对象始终位于作用域链的最前端。如果上下文是函数,则其活动对象(activation object)用作变量对象。活动对象最初只有 一个定义变量:arguments。(全局上下文中没有这个变量。)
作用域链中的下一个变量对象来自包含上下文,再下一个对象来自再下一个包含上下文。以此类推直至全局上下文;全局上下文的变量对象始终 是作用域链的最后一个变量对象。 - 代码执行时的
标识符解析是通过沿作用域链逐级搜索标识符名称完成的。搜索过程始终从作用域链 的最前端开始,然后逐级往后,直到找到标识符。(如果没有找到标识符,那么通常会报错。) - 增强作用域可以使用eval函数或with函数(
不建议)
自由变量
自由变量在定义时候
- 一个变量在当前作用域没有定义,但被使用了
- 向上级作用域(沿着作用域链),一层一层依次寻找,直到找到为止
- 如果到全局作用域都没有找到,则报错 xxx is not defined
变量提升(Hositing)
varfunction- 可以使用let、const避免
关于栈溢出(Stack Overflow)
关于this指向
普通函数(非箭头函数)
this 的指向是在调用时决定的,而不是在书写时决定的。
- 多数情况下,this 指向调用它所在方法的那个对象。
说得更通俗点, 谁调的函数,this 就归谁。 - 当调用方法没有明确对象时,this 就指向全局对象。在浏览器中,指向 window;在 Node 中,指向 Global。(严格模式下,指向 undefined)
特殊说明 三种特殊情境下,无论严格模式还是非严格模式,this会指向 window
- 立即执行函数(IIFE)
- setTimeout 中传入的函数
- setInterval 中传入的函数
箭头函数
箭头函数中的 this,可以采用函数中自由变量的寻找值方式寻找this值。(作用域寻值方式)
改变this指向方法
- bind
- call
- apply