LOGO 首页 OA教程 ERP教程 模切知识交流 PMS教程 CRM教程 技术文档 其他文档  
 
网站管理员

栈和堆:JavaScript 内存的“旅馆”和“仓库”

zhenglin
2026年7月6日 17:2 本文热度 39

对象和原始值到底存在哪?赋值和传参时,内存里发生了什么?为什么 React 强调“不可变更新”,而Vue3 可以直接修改 ref 的值?这些问题的答案,都藏在 JavaScript 内存的两个核心区域里:栈(Stack)堆(Heap),它们就像生活中的旅馆前台和仓库。

一、旅馆前台 vs 仓库——内存的两个“部门”

我们可以把JavaScript 引擎看作一家旅馆

1. 栈:旅馆前台

旅馆前台存放的是小件、轻便、存取快速的东西——比如信件、钥匙。前台空间有限,但取放极快。更重要的是,退房(函数执行结束)时,前台的物品会自动被清空,不需要专门打扫。

在 JavaScript 中,负责存放:

  • 原始值numberstringbooleannullundefinedsymbolbigint

  • 指向堆中对象的引用地址(也就是对象的“房间号”)

原始值大小固定、生命周期明确(随函数调用产生和销毁),非常适合放在栈里。


2. 堆:仓库

旅馆后面有一个大仓库,存放大件、笨重、需要长期保存的物品——比如家具、行李。仓库空间很大,但存取比前台慢,而且需要专门的人定期去清理长期不用的东西(垃圾回收)。

在 JavaScript 中,负责存放:

  • 引用类型:对象(Object)、数组(Array)、函数(Function)、闭包变量等

这些数据的大小不固定,可能非常庞大,而且经常需要在函数返回后依然存活(比如被挂载在全局对象上,或者被事件回调引用)。

两者区别:栈像旅馆前台,快而小,存原始值和房间号;堆像仓库,慢而大,存对象本身。


二、原始值与引用值——从前台和仓库的角度理解

1. 原始值:直接存在前台

let age = 25;

let name = 'weedsfly';

let isAdmin = true;

这些值都直接存储在栈上,每个变量占据一小块固定大小的空间。赋值时,复制的是值本身

let a = 10;

let b = a;   // 把 a 的值 10 复制一份给 b

b = 20;

console.log(a); // 10,不受影响

ab 各自拥有独立的栈空间,互不干扰。

2. 引用值:存在仓库,前台只留房间号

let user = { name: '张三', age: 30 };

user 变量存在栈上,但存的是对象在堆中的内存地址(房间号)。真正的对象 { name: '张三', age: 30 } 躺在仓库里。

当你“复制”一个引用值时:

const obj = { count: 0 };

obj.count = 1; // 合法,房间号没变

obj = {};      // 报错,尝试更换房间号

anotheruser 拿的是同一个房间号,指向仓库里的同一个对象。无论通过哪个变量去改房间里的东西,另一个变量也能看到变化。

这就是为什么 const 声明的对象内容可以被修改——const 锁住的是栈上的房间号,不是仓库里的物品。

const obj = { count: 0 };

obj.count = 1; // 合法,房间号没变

obj = {};      // 报错,尝试更换房间号

三、函数传参:到底是值传递还是引用传递?

这是一个经典的面试题。从内存模型的角度看,答案很清晰:JavaScript 永远是值传递,但传递的值可能是原始值,也可能是引用地址

function change(num, obj) {

  num = 100;

  obj.name = 'Changed';

}


let count = 1;

let person = { name: 'Original' };

change(count, person);


console.log(count);  // 1 — 未改变

console.log(person.name); // 'Changed' — 被改变了

  • count 是原始值,change 内部得到的是一个值副本,修改不影响外部。

  • person 是引用值,change 内部得到的是房间号的副本,但这个房间号依然指向堆里的同一个对象。所以修改对象内容,外部也会看到。

如果你在函数内部把 obj 指向一个新对象:

function change(obj) {

  obj = { name: 'New' }; // 把房间号换成了另一个

}

let person = { name: 'Original' };

change(person);

console.log(person.name); // 'Original',外部不受影响

因为你改变的只是内部变量持有的房间号,外部变量持有的房间号没有变。

四、这些知识如何影响日常开发?

1. React 的不可变更新

React 依赖 Object.is 浅比较 来判断状态是否变化。如果你直接修改对象内部属性:

// 错误

const [user, setUser] = useState({ name: '张三' });

user.name = 'weedsfly';

setUser(user); // 引用地址没变,React 认为状态未更新,不会重渲染

从内存的角度,你只改了仓库里的东西,栈上的房间号没变,React 无法察觉。

正确做法是创建一个新对象,替换房间号

// 正确

setUser({ ...user, name: 'weedsfly' }); // 新对象,新房间号

React 比较栈上的引用地址,发现变了,就知道要更新组件了。这也解释了为什么 Redux 的 reducer 必须返回新 state,而不是修改旧 state。


2. Vue 3 的响应式为什么可以“直接修改”?

Vue 3 使用 Proxy 代理对象。当你修改 state.count 时,Vue 拦截了这个操作,内部其实并没有依赖引用地址比较来判断变化。它通过依赖收集和派发更新,直接追踪到了哪个属性被修改。

所以 Vue 3 不需要像 React 那样强制不可变更新,但这并不意味着内存模型失效——对象依然在堆里,refreactive 依然持有的是引用。


3. 深拷贝 vs 浅拷贝

如果你不小心只复制了房间号(浅拷贝),就可能导致意想不到的副作用:

const original = { user: { name: '张三' } };

const copy = { ...original }; // 浅拷贝:只复制了 user 的房间号

copy.user.name = 'weedsfly';

console.log(original.user.name); // 'weedsfly',被影响了

要想完全断开引用,需要深拷贝,即为嵌套对象也创建新的仓库实体。

const deepCopy = JSON.parse(JSON.stringify(original)); // 其中一种深拷贝方式


五、总结

理解栈和堆,就理解了 JavaScript 数据存储和传递的核心规则。记住这几个要点:

  • 存原始值和引用地址,小而快,随函数调用自动清理。

  • 存对象和数组,大而灵活,由垃圾回收器管理。

  • 赋值和传参都是值复制:原始值复制数据本身,引用值复制房间号。

  • React 的不可变更新要求你替换栈上的房间号;Vue 3 的 Proxy 则不用。

  • 浅拷贝只复制第一层引用地址,深拷贝才能完全断开引用链。

下次当你写出 setState({ ...prev }) 或者困惑“为什么 const 对象还能被修改”时,不妨想想旅馆前台和仓库的比喻——栈上只是换了一张房卡,仓库里的东西还在那里。



阅读原文


该文章在 2026/7/6 17:02:46 编辑过
关键字查询
相关文章
正在查询...
点晴ERP是一款针对中小制造业的专业生产管理软件系统,系统成熟度和易用性得到了国内大量中小企业的青睐。
点晴PMS码头管理系统主要针对港口码头集装箱与散货日常运作、调度、堆场、车队、财务费用、相关报表等业务管理,结合码头的业务特点,围绕调度、堆场作业而开发的。集技术的先进性、管理的有效性于一体,是物流码头及其他港口类企业的高效ERP管理信息系统。
点晴WMS仓储管理系统提供了货物产品管理,销售管理,采购管理,仓储管理,仓库管理,保质期管理,货位管理,库位管理,生产管理,WMS管理系统,标签打印,条形码,二维码管理,批号管理软件。
点晴免费OA是一款软件和通用服务都免费,不限功能、不限时间、不限用户的免费OA协同办公管理系统。
Copyright 2010-2026 ClickSun All Rights Reserved  粤ICP备13012886号-1  粤公网安备44030602007207号