ECMAScript原始值与引用值
解释 JavaScript 原始值与引用值在赋值、比较、可变性和内存直觉上的核心差异。
[!info] related notes
ECMAScript原始值与引用值
这篇笔记只回答一个问题: JavaScript 里的值在创建后,为什么会表现出“有的像复制一份,有的像共用同一份对象”。
一句话定义
原始值直接以值本身参与赋值和比较,而对象这类引用值通常通过对象身份参与赋值、共享和比较。
原始值包括什么
stringnumberbooleannullundefinedsymbolbigint
这些值的共同点不是“都在栈里”,而是它们在语言语义上都表现为按值使用、不可原地修改。
引用值通常指什么
ObjectArrayFunctionDateRegExpMap/Set
它们的共同点也不是“长得像对象”这么简单,而是变量里通常保存的是“访问该对象的身份”,多个变量可以指向同一个对象。
先建立最重要的直觉
原始值是不可变的
let str = "hello";
str[0] = "H";
console.log(str); // "hello"
str = "Hello"; // 不是改了原字符串,而是让变量重新绑定到一个新值
原始值可以被重新赋值,但值本身不会被原地改写。
引用值通常是可变的
const user = { name: "Alice" };
user.age = 25;
console.log(user); // { name: "Alice", age: 25 }
这里变化的是对象内容,不是变量绑定本身。const 约束的是“不能重新指向别的值”,不等于“对象内部绝对不可变”。
赋值时为什么表现不同
原始值赋值更像复制值
let a = 1;
let b = a;
b = 2;
console.log(a); // 1
console.log(b); // 2
b = a 之后,a 和 b 各自持有自己的值。
引用值赋值更像复制引用
const a = { x: 1 };
const b = a;
b.x = 2;
console.log(a.x); // 2
console.log(b.x); // 2
b = a 复制的不是对象内容,而是“指向同一个对象”的引用。
比较时为什么结果常让人误判
原始值比较的是值
1 === 1; // true
"hi" === "hi"; // true
引用值比较的是身份
const a = { x: 1 };
const b = a;
const c = { x: 1 };
console.log(a === b); // true
console.log(a === c); // false
a 和 c 内容一样,但不是同一个对象,所以结果仍然是 false。
内存直觉应该怎么理解
为了建立心智模型,可以先这样理解:
- 原始值通常表现为“变量直接拿到值”
- 引用值通常表现为“变量拿到对象地址或身份,再去访问对象内容”
这个模型足够解释大多数赋值、比较和共享行为。真正的引擎实现会更复杂,但不影响日常判断。
这和垃圾回收有什么关系
引用值的关键不是“放在堆里”这句话本身,而是对象什么时候还可达。
- 只要还有变量、闭包、定时器、全局对象等路径能访问它,它就可能继续存活
- 当对象不再可达时,GC 才有机会回收它
延伸阅读: ecmascript-memory-management
为什么 React 会反复强调不可变更新
React 的状态更新、React.memo 和很多性能优化,都强依赖“新旧值是不是同一个身份”。
// 错误:原地修改,引用没变
user.age = 19
setUser(user)
// 推荐:创建新对象,让身份变化
setUser({
...user,
age: 19,
})
如果你直接改原对象,再把同一个引用传回去,React 很多比较逻辑会认为“还是原来那个值”。
常见误区
1. “引用类型” 不等于 “一定深拷贝才安全”
只有在你确实要切断共享修改时,才需要创建新对象。很多场景只需要浅拷贝一层就够了。
2. const 让对象不可变
错。const 只保证变量绑定不重新赋值,不保证对象内部不可改。
3. “原始值在栈、对象在堆” 就是完整真相
这只是入门心智模型。它能帮助理解行为,但不要把它当成 ECMAScript 规范层面的精确定义。
最短记忆方式
原始值主要看“值有没有变”,对象主要看“是不是同一个对象”。