我们写JS代码时天天和各种数据打交道,可很少有人知道这些数据藏在哪、小 v8 又是怎么有条不紊跑起代码的。接下来我们一起弄懂数据分类搭配V8底层执行逻辑,真正吃透JS基础内核。
一、V8引擎完整执行流程
在讲数据类型之前,我们先来搞明白
V8作为JS专属运行引擎,做事条理十足,靠着一套固定流程推进代码运行,全程井然有序。
- 创建调用栈 代码开始运行的第一件事,就是搭建好调用栈,它就像一个有序收纳架,专门用来收纳管理各类预编译流程与运行环境。
- 压入执行上下文 根据代码运行状态,往调用栈里放入对应的执行上下文。运行全局代码就放入全局上下文,调用函数时就压入函数专属上下文,前期所有预编译准备工作,全都依托上下文完成。
- 逐行执行分配内存 正式运行代码时,引擎自动智能区分数据
我们来举一个例子
function foo(){
function fn(){}
fn()
}
foo()
上面这段代码的调用栈如图。
运行全局代码后,先创建全局执行上下文。
我们在全局进行了foo函数的调用foo(),于是 v8 接着创建了 foo 函数执行上下文。 foo 函数中又进行了fn函数的调用fn(),所以接着创建 fn 函数执行上下文。
执行完毕后,从上到下进行销毁。
栈就像你牛仔裤的裤子口袋,从下往上堆东西,从上往下取东西,想要取到下面的东西,就必须把上面的东西先拿出来。
在这里,我们先省略内存的分配,在认识 JS 两大数据类型后,我们会对内存的分配有更深刻的理解。
二、JS两大数据类型
1. 基本类型
这类数据体量小巧玲珑,结构简单没有多余内容,就像随身带的小物件,轻便不占地方,直接存放于栈内存,存取速度飞快。
var声明的变量存放在变量环境中,let和const声明的变量存放在词法环境中。
一共包含七种:
- 字符串 string
使用单引号'...' 用于存放字符
let str = 'hello world' // string 类型
let str2 = str + ' js'
//let str2 =`${str} js` 在字符串后面加字符的另一种方法
我们可以使用以下这些方式来读取字符
// 读取
console.log(str2);
console.log(str[0])
console.log(str.at(0))
console.log(str.slice(0,5)) // 左闭右开,截取字符串
- 数字 number
JavaScript 中不区分int、float等类型,整数和小数统一使用number
let num = 123
let num2 = num + 0.1
如果用number加字符,number会被 v8 偷偷的转化成 String类型
console.log(num2 + '5') // 输出123.15
这也太不公平了,为什么要把数字类型转成字符串类型呢,我们能不能把字符串类型转成数字类型呢?
答案也是可以的,对应将数字类型转成字符串类型的num2.toString(),我们可以用Number()将字符串类型转成数字类型
let m = '2'
console.log( 1 + Number(m)) // 字符串转换成数字
不过前提是被转换的字符串当中是一个数字,不然就会输出NaN,NaN也被认为是一个数字
let p = '2p'
console.log( 1 + Number(p)) // NaN not a number
- 大整数 bigInt
number 类型 2^53 是最大安全值,为了存放大于安全值的数字,我们引入了大整数 bigInt,在整数后面加n,两个大整数相加也是大整数
let a = 4637647393478734563223233455197n
- 布尔值 boolean
true和false,放在if语句括号里面的值会被 v8 悄咪咪的转化成 boolean 类型,在所有数字里面,只有0和NaN放在if中被认为是false
- 唯一标识 Symbol
Symbol是全世界独一无二的值,用来做唯一标识,不能与其它类型进行运算
let s1 = Symbol('a')
let s2 = Symbol('a')
console.log(s1 == s2); // false
console.log(s1 === s2); // false
- 未定义 undefined
表示 应该有值,但还没给 / 不存在
- 空值 null
代表对象引用为空、无实际对象,可以用来清空对象,或者使用空数据占位
值得一提的是
null == undefined // true 松散相等
null === undefined // false 严格不等
在这里,==是相等,===是全等,指的是在值相等之外,类型也要相等
2. 引用类型
这类数据体量可大可小,内部还能层层嵌套,如同体积庞大的大件物品,没办法直接塞进狭小的栈空间。
那么 v8 会怎么办呢?
引擎会把真实数据本体存放到宽敞的堆内存中,再生成一串简短的内存地址,最后只把这串地址放进栈内存里。
我们操作数据时,都是通过栈内地址,间接找到堆里的真实内容。
接下来我们来看一个综合一点的例子,便于理解 V8 引擎完整执行流程,和内存的分配:
var a=1
let b='hello'
var obj={
name:'萧',
age:18,
like :{
one:'eat',
two:{
sport:['篮球','跑步']
}
}
}
console.log(obj);
上面代码的调用栈如图:
执行代码,v8 将原始类型的值存入栈中,遇到引用类型的值,则将值存入堆空间并生成一个引用地址,将引用地址存入栈中。
常见引用类型有:
- 函数Function
- 日期对象Date
- 普通对象Object
- 数组Array
另外补充一些数组的增删改查:
var arr =['a','b',1,2]
arr.push(3) // 在数组尾部添加元素
arr.pop() // 移除数组尾部元素
arr.unshift(0) // 在数组头部添加元素
arr.shift() // 移除数组头部元素
arr.splice(1,1) // 在中间移除元素,第一个参数是下标,第二个参数是移除个数
arr.splice(2,0,2) // 在中间插入元素,第一个参数是下标,第二个参数是移除个数 ,第三个参数是要插入的元素
a[2]=10 // 更改元素值
三、栈堆分离存储的实用好处
V8采用这种内存分配方式,设计十分巧妙,优势格外明显。
- 既不用刻意扩容栈内存,保持栈体小巧轻便,稳稳拉高引擎整体运行效率;
- 又能把大容量复杂数据安置在堆内存,从根源上避免数据过多挤占空间引发爆栈问题;
- 快慢内存相互配合,兼顾了基础数据的读取速度,和复杂数据的存储灵活性,让代码运行流畅又稳定。
总结
简单来说,小巧基础数据住栈内存,庞大复杂数据本体住堆、地址留栈
V8依靠调用栈规整管理执行环境,完成预编译后正式运行代码。
一快一稳的内存搭配模式,让JavaScript兼顾运行速度与内存利用率,也是代码能够稳定高效运行的底层关键。