浏览器原理第二篇:站在浏览器的角度,分析JavaScript执行机制;深入探讨来自Google的JavaScript引擎V8,分析其执行流程,细化讲解JavaScript中数据是如何存储和回收的,助你打造高性能且节约内存的Web应用。
07. 变量提升
08. 调用栈
编译执行
- 当 JS 执行全局代码时,会编译全局代码并创建全局执行上下文。
- 当调用一个函数时,函数体内部的代码会被编译,创建函数执行上下文。一般函数执行结束之后,创建的函数执行上下文会被销毁。
- eval 函数(执行时么?)的代码也会被编译,并创建执行上下文。
console.trace() 输出当前函数调用关系。
var a = 2;
function add(b, c) {
return b + c;
}
function addAll(b, c) {
var d = 10;
result = add(b, c);
return a + result + d;
}
addAll(3, 6);
09. 块级作用域
解决变量提升、变量覆盖、变量污染等 JS 设计缺陷。
作用域
指程序中定义变量的区域,该位置决定了变量的生命周期。通俗讲,作用域就是变量与函数的可访问范围,即作用域控制着变量和函数的可见性和生命周期。
全局作用域
在代码中的任何地方都能访问,其生命周期伴随着页面的生命周期。
函数作用域
在函数内部定义的变量或函数,定义的变量和函数只能在函数内部被访问。函数执行结束之后,函数内部定义的变量会被销毁。
块级作用域
使用一对大括号包裹的一段代码。
JS 将块级作用域放到词法环境。(编译阶段,let const 声明的变量放到词法环境,而 var 声明的变量放到变量环境中的。)
10. 作用域链和闭包
词法作用域是指作用域是由代码中函数声明的位置来决定的,是静态作用域,与函数调用无关。
闭包
在 JavaScript 中,根据词法作用域的规则,内部函数总是可以访问其外部函数中声明的变量,当通过调用一个外部函数返回一个内部函数后,即使该外部函数已经执行结束了,但是内部函数引用外部函数的变量依然保存在内存中,我们就把这些变量的集合称为闭包。比如外部函数是 foo,那么这些变量的集合就称为 foo 函数的闭包。
11. this
this 与作用域链基本无关。每个执行上下文中都有一个 this。
- 函数被正常调用时,函数中的 this 指向 window,严格模式下 this 是 undefined;
- 函数作为对象的方法调用时,函数中的 this 就是该对象;
- 可以使用 call、apply、bind 方法显式设置函数执行上下文的 this;
- 嵌套函数中 this 不会继承外层函数的 this 值(解决方案:self 保存外层 this 或 箭头函数);
- 被 setTimeout 推迟执行的回调函数是某个对象的方法,那么该方法中的 this 关键字指向 window;
- 构造函数的 this 指向新对象。
12. 栈空间和堆空间
语言类型
- 静态语言:在使用之前需要确定变量数据类型的语言。C、C++、Java
- 动态语言:在运行过程中需要检查数据类型的语言。JS、PHP、Python、Ruby、VB
- 弱类型语言:支持隐式类型转换的语言。C、C++、JS、PHP、VB
- 强类型语言:不支持隐式类型转换的语言。Java、Ruby、Python
栈
原始类型的数据存放在栈中。(变量名:变量值)(a: 1)(user: 1003)
堆
引用类型的数据存放在堆中。 (地址:值)(1003: { name: 'jkb', age: 2 } )
再谈闭包
编译过程中,遇到内部函数应用外部函数的变量,JS 引擎判断是一个闭包,会在堆空间创建一个内部对象 "closure(foo)" ,将用到的变量保存在"closure(foo)"中。
13. 垃圾回收
C、C++:使用手动回收策略,何时分配内存、何时销毁内存都是有代码控制的。
Javascript、Java、Python:使用垃圾回收器来释放产生的垃圾数据。
调用栈的数据回收
函数执行结束之后,JS 引擎通过向下移动 ESP(记录当前执行状态的指针)来销毁该函数保存在栈中的执行上下文。当新的函数执行上下文入栈时,直接覆盖原来的执行上下文。
堆中数据回收
函数执行完毕,栈内存释放。但是函数中对象使用的堆空间仍未释放,这就需要 JS 的垃圾回收器。
V8
两个堆的区域
- 新生代:存放的是生存时间短的对象,容量 1~8M。
- 老生代:存放生存时间久的对象。
两种垃圾回收器
- 副垃圾回收器:主要负责新生代的垃圾回收。
- 主垃圾回收器:主要负责老生代的垃圾回收。
共同执行流程
- 标记空间中活动对象和非活动对象。活动对象是还在使用的对象,非活动对象是可以进行垃圾回收的对象。
- 回收非活动对象占据的内存。即在标记完成后,统一清理内存中被标记为可回收的对象。
- 内存整理。频繁回收对象后,会存在大量不连续空间--内存碎片,最后一步需要整理这些内存碎片。(主垃圾回收器)
副垃圾回收器
新生代使用 scavenge 算法,将空间对半划分为对象区域、空闲区域。新加入的对象都存放在对象区域,对象区域写满时,开始一次垃圾清理操作。
- 给对象区域中的垃圾做标记,标记完成后进入垃圾清理阶段;
- 副垃圾回收器将存活的对象复制到空闲区域,并把它们有序的排列起来(无内存碎片);
- 复制完成后,对象区域和空闲区域进行角色反转;
- 如果经过两次垃圾回收依然还存活的对象,会被移动到老生区。
主垃圾回收器
老生区中对象的特点:占用空间大、存活时间长。
标记-清除算法(Mark - Sweep)
- 垃圾标记过程:从一组根元素开始,遍历,能到达的称为活动对象,没有达到的判为垃圾数据。
- 垃圾清除过程:直接对可回收对象进行清理。
标记-整理算法(Mark - Compact)
标记过程一样,然后让所有存活对象向一端移动,最后直接清理端边界外的内存。
全停顿
JS 和垃圾回收都是运行在主线程上的。一旦执行垃圾回收,JS 脚本会暂停,待垃圾回收完毕后再恢复脚本执行,这种行为被叫做全停顿。
为降低老圣地啊的垃圾回收造成的卡顿,V8 将标记过程分为一个个的子标记过程,同时让垃圾回收标记和 JS 应用逻辑交替进行,直到标记完成,这个算法叫增量标记算法。
14. 编译器和解释器
编译器
编译型语言,在程序执行之前,编译器将源代码编译成而二进制文件,每次运行程序时,直接运行二进制文件,无需再编译。如 C、C++、GO
工作流程:
- 源代码 词法分析、语法分析 >> AST;
- AST 词义分析 >> 中间代码;
- 中间代码 代码优化 >> 二进制文件,直接执行。
解释器
每次运行时需要通过解释器对程序进行动态解释和执行。如 Python、JS。
工作流程:
- 源代码 词法分析、语法分析 >> AST;
- AST 词义分析 >> 字节码,解释执行。
AST
JS 执行代码第一步,生成 AST 和执行上下文。
- 词法分析,将一行代码拆分成一个个 token(如 keyword、identifier、assignment 赋值、literal 字符串...);
- 语法分析,将生成的 token 数据,根据语法规则转为 AST,如果源码符合语法规则就会顺利完成,如果存在语法错误就终止并抛出"语法错误"。
字节码
JS 执行代码第二步,生成字节码。
- 解释器 Ignition 根据 AST 生成字节码。
- 解释器 Ignition 逐条解释执行字节码。发现热点代码(被重复执行多次),后台的编译器 TurboFan 会将热点代码编译为高效的机器码并保存备用。
字节码介于 AST 和机器码之间,需要解释器将其转换为机器码才能执行。内存占用比机器码少。
字节码配合解释器和编译器的技术被称为即时编译 (JIT)。
JS 性能优化
- 提升单次脚本执行速度,避免 JS 长任务霸占主线程。
- 避免大的内联脚本。
- 较少 JS 文件的容量。
附:
babel 工作原理
先将 ES6 源码转换成 AST,然后再将 ES6 语法的 AST 转换为 ES5 语法的 AST,最后利用 ES5 的 AST 生成 JS 源码。
WebAssembly
assembly程序集。
一种技术方案,使用非JS编程语言编写代码并能在浏览器运行。
使用C、C++、Rust编写代码,使用LLVM(编译器工具链)编译成.wasm文件(可在JS中加载使用)。
JS解释器和编译器
将JS代码翻译成机器语言。
- 解释器:翻译过程是一行一行进行的,执行到哪行,翻译哪行。
- 编译器:执行前翻译。
监视器
JS引擎的一部分。在JS运行时监控代码,记录代码片段运行的次数以及使用的数据类型。运行几次的被标记为"warm",交由基础编译器(小幅提速);运行很多次的被标记为"hot",交给优化编译器(大幅提速)。
JS vs WebAssembly
JS:download >>> parse >>> compile + optimize >>> re-optimize >>> execute >>> garbage collection
WebAssembly:download >>> parse >>> compile + optimize >>> execute
- download:相同功能,WebAssembly文件比JS文件小。
- parse:JS源码先被解析为抽象语法树,然后需要的代码被转换为字节码,不需要的被创建存根。WebAssembly是字节码,只需要被解析并确定有没有错误。
- compile + optimize:JS动态类型语言,执行期间编译,相同代码多次执行可能因数据类型不同被重新编译。而WebAssembly字节码不需要在运行时判定数据类型。
- re-optimize:JS代码基于运行时的假设不正确是,会重新优化。WebAssembly类型明确不需要重新优化。
- execute:WebAssembly为编译器设计,提供更适合机器的指令,运行速度更快。
- garbage collection:JS引擎使用垃圾回收器自动进行垃圾回收(无法控制时机)。WebAssembly不支持垃圾回收,内存需要手动管理