创见博客
JS 基本类型和引用类型存在哪?栈和堆有什么区别
七崽爱吃小饼干2026/10/11阅读 0

同样是赋值,一个改了原值不变,一个改了原值跟着变:

js
let a = 1
let b = a
b = 2
console.log(a) // 1,互不影响

let o1 = { n: 1 }
let o2 = o1
o2.n = 2
console.log(o1.n) // 2,改的是同一块内存

这个差异通常被归结为一句话:"基本类型存在栈里,引用类型的数据存在堆里,变量在栈里存了一个地址。"这句话在面试里够用,但只对了一半。这篇文章先把标准答案讲清楚,再说清楚它哪里不准确。

一、JS 的数据分两类

  • 原始类型(Primitive):Undefined、Null、Boolean、Number、String、Symbol、BigInt,共 7 种。不可变,按值访问。
  • 引用类型(Object):对象、数组、函数、日期、正则、Map/Set 等,统称 Object。可变,按引用(地址)访问。
js
let a = 1
let b = a
b = 2
console.log(a) // 1

let o1 = { n: 1 }
let o2 = o1
o2.n = 2
console.log(o1.n) // 2

复制时,原始类型复制的是值本身,引用类型复制的是地址,两个变量最终指向堆上同一个对象。

二、栈和堆分别是什么

这里的"栈"和"堆"是内存区域,不是数据结构教科书里的同名概念,但确实借用了它们的特点。

维度栈(Stack)堆(Heap)
存放内容执行上下文、局部变量、函数调用信息动态分配的对象、数组等引用类型数据
分配/释放由引擎自动管理,随函数调用进入和退出动态分配,交给垃圾回收器(GC)回收
访问速度快(连续内存,push/pop 语义)慢(需要查找、可能产生碎片)
容量小,溢出会报 Maximum call stack size exceeded大,受可用内存限制
大小确定编译期/进入函数时基本确定运行时才知道,可动态增长

栈像一个叠盘子的容器,进出的顺序固定,所以又快又简单;堆像一块自由仓库,对象往哪放、放多大运行时才知道,灵活但管理成本高。

三、标准答案:变量怎么存

按常见说法:

js
let count = 42          // count 的值 42 直接放在栈上
let user = { name: 'x' } // user 在栈上存一个地址,对象 { name: 'x' } 放在堆上

用图表示:

text
栈                          堆
┌──────────────┐        ┌──────────────────┐
│ count = 42   │        │                  │
├──────────────┤        ├──────────────────┤
│ user = 0xA1 ─┼───────▶│ { name: 'x' }    │
└──────────────┘        │  @ 0xA1          │
                        └──────────────────┘

于是就有了它的三条推论:

  1. 原始类型赋值是"拷贝值",改了互不影响。
  2. 引用类型赋值是"拷贝地址",两个变量共享同一个对象。
  3. 函数传参时,原始类型传值,引用类型传地址(是值的拷贝,不是引用传递)。

四、函数传参的例子

js
function change(primitive, obj) {
  primitive = 10   // 只改了函数内的副本
  obj.n = 99       // 通过地址改了堆上的对象
}

let x = 1
let o = { n: 1 }
change(x, o)

console.log(x)     // 1,没变
console.log(o.n)   // 99,变了

注意一个常见误区:如果把参数指向新对象,外部也不会跟着变,因为函数内改的是栈上那个地址副本:

js
function reassign(obj) {
  obj = { n: 5 }   // 只是让函数内的副本指向新对象
}
let y = { n: 1 }
reassign(y)
console.log(y.n)   // 1,外部不受影响

JS 只有按值传递。引用类型"传引用"的说法,准确讲是"传地址这个值"。

五、这句话哪里不准确

面试答上面那套没问题,但再深一层要补两点,避免被追问翻车。

第一,V8 里字符串其实是堆上的对象。 String 是原始类型没错,但引擎实现里字符串(尤其是拼接产生、较长的字符串)通常以堆对象形式存在,靠不可变和内部化(interning)复用。所以"原始类型都在栈上"并不严格。

js
const s = 'hi'
s === 'hi' // true,相同字面量会被内部化复用

第二,闭包捕获的变量会被搬到堆上。 局部变量本该随函数栈帧销毁,但如果被闭包引用,它必须活得更久,引擎会把它分配到堆上的上下文对象里,而不是留在栈上:

js
function counter() {
  let count = 0        // 通常的局部变量在栈上
  return function () {
    count++            // 被闭包捕获后,count 实际上活在堆上的上下文中
    return count
  }
}
const inc = counter()
inc() // 1
inc() // 2

栈帧在 counter() 返回后就弹出了,count 却还在——这说明它绝不可能只存在栈上。

第三,引擎还会做优化。 局部原始值可能直接放在寄存器里,短命对象会被逃逸分析优化到栈上分配,所以真实的存储位置是引擎实现细节,随版本和优化策略变化。

一句话总结准确版本:变量(包括原始值和引用地址)都活在执行上下文里;可变的对象数据一般在堆上。栈和堆只是对引擎内存管理的简化描述。

六、为什么要有这种划分

如果所有数据都堆在栈上,会有两个问题:

  • 栈要求大小在进入时就确定,而对象可以动态增删属性、随时变大,栈放不下。
  • 栈帧随函数退出就要销毁,而对象需要更长的生命周期,由 GC 在合适时机回收更合适。

反过来,如果所有数据都放堆上,连 let i = 0 这种临时变量都要走堆分配和 GC,性能会明显变差。所以栈负责小而短命的数据,堆负责大而灵活的数据,各司其职。

七、速查表

对比项栈(Stack)堆(Heap)
主要存放执行上下文、局部变量、调用信息对象、数组等引用类型数据
管理方式引擎自动分配释放GC 回收
速度快慢
容量小,会栈溢出大
大小进入时基本确定运行时动态确定
访问方式直接按值先取地址再访问
行为原始类型引用类型
变量里存什么值本身指向堆的地址
赋值拷贝值拷贝地址
修改副本不影响原值影响原始对象
传参传值传地址(按值传递)
可变性不可变可变

小结

  • 标准答案:基本类型的值在栈上,引用类型的数据在堆上,变量在栈上存地址。
  • 赋值和传参的差异,本质是"拷值"还是"拷地址",JS 只有按值传递。
  • 更准确的版本:变量活在执行上下文里,可变对象数据在堆上;字符串、闭包捕获的变量、引擎优化都会打破"原始类型都在栈上"的简单说法。
  • 划分的动机是分工:栈管小而短命的数据,堆管大而灵活的数据。
评论
0/100