探秘JavaScript 类型系统与其决定的不同V8 执行机制

110 阅读6分钟

一、引言

JavaScript 这门动态弱类型语言,它的类型系统看似简单,却有许多易忽略的细节。同时,理解 V8 引擎如何执行代码、如何在内存中用堆与栈存储不同类型的数据,便可以让我们能轻松应对内存管理等常见问题。

云宝将本文的主体分为两部分:先梳理 JavaScript 中的两大类型的阵营,再深入 V8 引擎的执行过程与栈堆内存如何在底层调用。


二、JavaScript 中的类型

JavaScript 的类型分为两大类:基本类型(原始类型)  和 引用类型(复杂类型)

1、基本类型(原始类型)

1. string

首先我们来声明一个简单的字符串的变量str:let str = 'hello'

这时如果我们想把这个字符串变为“hello javascript”便有了以下两种方法,都可以用console.log(str2)输出hello js

  1. let str2 = str + ' js'
  2. let str2 = '${str} js'

下面是几个字符串有关的不同的输出表达式来满足对应的输出需求

  • console.log(str)-----------------直接输出str字符串
  • console.log(str[0])--------------输出第一个字符(即h)
  • console.log(str.at(0))-----------输出第一个字符(即h)
  • console.log(str.slice(0,7))------输出前7个字符,遵循左闭右开规则,此处输出第0123456个字符(即hello w)

2. number//含NaN

一样的我们先声明并赋值一个简单的numberlet num = 123; 基于此我们便可以进行简单的数学运算:

  • let num2 = num + 0.1 //此处注意在js里0.1是number类型,无浮点类

此外依然有一些零散知识碎片:

  • console.log(num.toString())--将number类转string类

  • console.log(num + '5')--------此时v8会悄悄将num转为string类型,因此结果为’1235‘,而不是进行数学运算相加

  • console.log(Number(m))------字符串转数字(注意Number里N大写)

  • let n = 1; let m = 'm';

此时console.log(Number(n+m))输出NaN,因为此时不能转化为实际数字,只能转化为特殊的number类型NaN

  • 此外在计算2的53次方大的数字时会失真

另外if( )里只有0和NaN才会被动转换成flase,其他都为ture

云宝此外进行拓展一下:

  • let b = 1
  • let c = '1'
  • console.log(b == c)
  • console.log(b === c)
  • == 是相等运算符,会进行类型转换:将字符串 '1' 转为数字 1,与 1 比较,结果为 true
  • === 是严格相等运算符,不进行类型转换:number 和 string 类型不同,直接返回 false

3. boolean

布尔类型即boolean只有turefalse两个值,下边这个代码输出结果便很直白,为‘云宝’

  • if(ture){ console.log('云宝') }

4. undefined

这是一个特殊类型,我们若声明一个undefined类型的值let u = undefined,此时u的值是undefined,类型也是undefined


5. null

null 代表 “空值”“没有对象” 。它通常用于主动表示一个变量当前没有指向任何对象。

注意:null 与 undefined 的区别:

  • undefined 表示变量已声明但未赋值。
  • null 表示变量已赋值,但值为“空”。

6. bigInt

bigint 是引入的新基本类型,用于表示任意精度的大整数。它解决了 number 类型无法精确表示超过 2^53 - 1 的整数的问题。两个bigint类型可以进行运算!

const bigNum1 = 9007199254740993n;           // 超过 2^53 - 1
const bigNum2 = 12345678901234567890n;       // 更大的大整数
// 相加
const sum = bigNum1 + bigNum2;
// 输出结果
console.log(sum);                // 12354687900489318883n

7. Symbol

这是一个特殊的基本类型,该类型各个值都不会相等,每一个值都不会一样,举个例子:

  • let s = symbol('hello')
  • let p = symbol('hello') //s与p不相等

这样可以有效防止定义参名时重复套用哦~

2、引用类型(复杂类型)


1. Array

数组像是进行线性存储的管道,我们首先创建一个数组,并对它进行使用:

var arr = ['a','b',1,2 ]
arr.push(3)//尾部推入3
console.log(arr)//输出a,b,1,2,3
arr.pop()//尾部删除3
console.log(arr)//输出a,b,1,2
arr.unshift(0)//头部加入0
console.log(arr)//输出0,a,b,1,2
arr.shift()//头部删除
console.log(arr)//输出a,b,1,2
arr.splice(1,1)//用于删除元素,第一个数是位置,第二个是长度
console.log(arr)//输出a,1,2
arr.splice(2,0,0)//用于增加元素,找到2号下标,移除0个值,插入0进去
console.log(arr)//输出a,1,0,2
arr[2] = 10//将第三个数改为10;
console.log(arr)//输出a,1,0,10

这就像一个无限长的存储条,可以进行任意操作,但是过长的话会大幅增加操作的时间复杂度


2. Object

是一组键值对的集合,理论上可以进行无限嵌套,没有对象?我们先来创建一个!

var a = 1
var b = 'hello'
var obj = {
    name:'rainbow',
    age: 18,
    like:{
        one:'eat',
        two:{
            sports:['1','2']
        }

    }
}
obj.Friend = 'applejack'
delete obj.Friend 

console.log(obj)的话会把该对象的所有属性值都罗列。


3. Function

function函数依然理论上可以在里面塞入无限运行逻辑与语句,是一段可重复执行的代码块

var fn = function(){

}

4. Date

特殊的引用类型,我们可以在浏览器查看页面中看到:

image.png

三、V8 引擎的底层执行机制

1. 创建调用栈

当 JavaScript 代码开始执行时,V8 首先创建一个调用栈。它是一个 后进先出 的数据结构,用于管理函数调用的执行上下文。

2. 存入执行上下文

  • 每当执行一段全局代码时,会创建一个全局执行上下文并压入栈底。

执行上下文中包含了变量环境、词法环境等重要信息。

3. 执行代码并分配内存

在执行代码的过程中,V8 根据变量类型决定内存分配方式:

栈(Stack)中存放:

  • 基本类型的值:直接保存在栈内存中。
  • 引用类型的引用地址:即指向堆内存中实际对象的指针。

例如:

javascript

let name = "rainbow";     // 栈中存 "rainbow"
let age = 19;           // 栈中存 19
let obj = { name: "applejack", age: 20 };  // 栈中存引用地址,比如 #001

堆(Heap)中存放:

  • 引用类型的实际数据obj 指向的对象 { name: "applejack", age: 20 } 存储在堆内存中。
image.png

为什么要这样设计?

  • 栈的内存大小有限,且访问速度快,适合存放大小固定、生命周期明确的数据(如基本类型和引用地址)。如果把所有对象都塞进栈,很容易导致爆栈。
  • 堆的空间大且灵活,适合存放大小不确定或生命周期较长的对象。通过引用地址的方式,栈只保留一个小小指针,既保证了栈的轻量高效,又不会使栈内存过度膨胀。

**** ** 今日学习完毕,云宝给大家点赞 ******