V8 是一个由 Google 开发的开源 JavaScript 引擎,目前用在 Chrome 浏览器和 Node.js中。我们可以简单将 JavaScript 引擎理解成是一个翻译程序,将人类能够理解的JavaScript,翻译成机器能够理解的机器语言。
栈空间和堆空间:数据是如何存储的?
首先我们先看两段代码:
function apple() {
var a = 1
var b = a
a = 2
console.log('a: ', a)
console.log('b: ', b)
}
apple()
function foo() {
var a = { name: 'sunny' }
var b = a
a.name = '晴天'
console.log('a:', a)
console.log('b:', b)
}
foo()
我们看一下输出结果,来分析一下。
第一段代码 a: 2,b: 1。这没什么问题。执行第二段代码,就会发现,我们仅仅只是改变了 a 的值,但是 a 和 b 的输出是一样的,b 也被修改了,这和我们预期的不一样。
JavaScript 是什么类型的语言?
int main() {
int a = 1;
char* b = "sunny";
bool c = true;
return 0;
}
对于 C 语言,我们在定义变量之前,是需要先定义类型的,而对于 JavaScript,我们就不需要再声明变量之前确认其数据类型。
在声明变量之前需要先定义变量类型。我们把这种在使用之前就需要确认其变量数据类型的成为静态语言,相反地,我们把在运行过程中需要检查数据类型的语言称为动态语言。
在前面代码中,我们可以执行
c = a
将 int 类型的变量赋值给布尔值,这段代码也是可以编译执行的。因为在编译中,会发生隐式类型转换,将int型的变量悄悄转换成 bool 型的变量。支持隐式类型转换的语言称为弱类型语言,不支持隐式类型转换的语言称为强类型语言。参考:
那么,我们现在知道了,JavaScript是一种弱类型、动态的语言。
弱类型,意味着你不需要告诉JavaScript引擎变量的数据类型,它会在运行代码的时候算出来。
动态,意味这可以使用同一变量保存不同类型的数据。
JavaScript 的数据类型
JavaScript 的数据类型,8种:
类型 | 描述 |
---|---|
Boolean | 有两个值:true、false |
null | 一个表明 null 值的特殊关键字 |
undefined | 和 null 一样是一个特殊的关键字,undefined 表示变量未赋值时的属性 |
Number | 整数或浮点数,例如: 42 或者 3.14159 |
BigInt | 可以安全地存储和操作大整数,甚至可以超过数字的安全整数限制 |
String | 字符串是一串表示文本值的字符序列,例如:"sunny" |
Symbol | 一种实例是唯一且不可改变的数据类型(ES6新增的) |
Object | 对象,可以看作一组属性的集合 |
需要注意的三点内容:
- 使用
typeof
检测null类型时,返回的是 Object - Object 类型比较特殊,它是由上述7种类型组成的一个包含了 key-value 对的数据类型
- 前7种为原始类型,最后一种为引用类型。区分这两种,是因为它们在内存中存放的位置不一样
内存空间
主要有三种类型空间:代码空间、栈空间、堆空间。
原始类型的数据值都是直接保存在“栈”中的,引用类型的值是存放在“堆”中的。
因为 JavaScript 引擎需要用栈来维护程序执行期间的上下文状态,如果栈空间大了的话,所有的数据都存放在栈空间里面会影响上下文切换的效率,进而影响到整个程序的执行效率。
通常情况下,栈空间都不会设置太大,主要用来存放一些原始类型的小数据。而引用类型的数据占用的空间比较大,这类数据会存放在堆中,堆的空间很大,可以存放很多大的数据,不过缺点是分配内存和回收内存会占用一定的时间。
在 JavaScript 中,赋值操作和其他语言有很大不同,原始类型的赋值会完整复制变量值,而引用类型的赋值则是复制引用地址。
再谈闭包
var a = 0
var func1 = function () {
var b = 0
var inner = function () {
a = a + 1
b = b + 1
return b
}
return inner
}
console.log(`outer: a=${a}`) //0
var func2 = func1();
for (var i = 0; i < 2; i++) {
console.log(`func2: b= ${func2()}, a=${a} `) // 1,1 2,2
}
var func3 = func1();
for (var i = 0; i < 2; i++) {
console.log(`func3: b= ${func3()}, a=${a} `) // 1,3 2,4
}
垃圾回收:垃圾是如何自动回收的?
JavaScript 是使用自动垃圾回收的策略的,它产生的垃圾数据是由垃圾回收器来释放的,不需要手动回收。
调用栈中的数据是如何回收的
当一个函数执行结束后,JavaScript 引擎会通过向下移动 ESP 来销毁该函数保存在栈中的执行上下文。
堆中的数据是如何回收的
要回收堆中的数据,就需要用到 JavaScript 中的垃圾回收器了。
代际假说和分代收集
代际假说的特点:
- 大部分对象在内存中存在的时间很短,简单来说,很多对象一经分配内存,很快就变得不可访问;
- 不死的对象会获得更久
在 V8 中会将堆分成新生代和老生代两个区域,新生代存放的是生存时间短的对象,老生代中存放生村时间就的对象。
对于这两个区域,V8 分别使用两个不同的垃圾回收器,以便高效地实施垃圾回收: 副垃圾回收器,主要负责新生代的垃圾回收 主垃圾回收器,主要负责老生代的垃圾回收
垃圾回收器的工作流程
不论什么垃圾的回收器,它们都有一套共同的执行流程:
- 标记空间中活动对象和非活动对象。所谓活动对象,就是还在使用的对象;非活动对象,就是可以回收的对象
- 回收非活动对象所占据的内存。其实就是在所有的标记完成后,统一清理内存中所有被标记为可回收的对象
- 内存整理。当内存中中出现大量内存碎片(即不连续的内存空间),需要整理这些内存碎片
编译器和解释器:V8是如何执行一段JavaScript代码的?
解释执行,需要将先将输入的源代码通过解析器编译成中间代码,之后直接使用解释器解释执行中间代码,然后直接输出结果:
编译执行,需要先将源代码转换成中间代码,然后我们的编译器再将中间代码编译成机器代码。通常编译成的机器代码是以二进制文件形式存储的,需要执行这段程序的时候,直接执行二进制文件就好了。还可以使用虚拟机将编译后的机器代码保存在内存中,然后直接执行内存中的二进制代码。
V8使用 JIT(Just In Time)技术,即混合使用编译器和解释器的技术。这是一种权衡策略,解释执行的启动速度块,但是执行的速度慢,而编译执行的启动速度慢,但执行时的速度快。
主要流程包括:
- 初始化基础环境
- 解析源码生成AST和作用域
- 依据AST和作用域生成字节码
- 解释执行字节码
- 监听热点代码
- 优化热点代码为二进制的机器代码
- 反优化生成的二进制机器代码