[Medium] 📄 Primitive vs Reference Types
1. What are Primitive Types and Reference Types?
什么是原始类型(Primitive Types)和引用类型(Reference Types)?
JavaScript 的数据类型分为两大类:原始类型和引用类型。它们在内存存储方式和传递行为上有本质的差异。
原始类型(Primitive Types)
特点:
- 存储在**栈(Stack)**中
- 传递时复制值本身(Call by Value)
- 不可变的(Immutable)
包含 7 种:
// 1. String(字符串)
const str = 'hello';
// 2. Number(数字)
const num = 42;
// 3. Boolean(布尔值)
const bool = true;
// 4. Undefined
let undef;
// 5. Null
const n = null;
// 6. Symbol(ES6)
const sym = Symbol('unique');
// 7. BigInt(ES2020)
const bigInt = 9007199254740991n;
引用类型(Reference Types)
特点:
- 存储在**堆(Heap)**中
- 传递时复制引用(内存地址)(Call by Reference)
- 可变的(Mutable)
包含:
// 1. Object(对象)
const obj = { name: 'John' };
// 2. Array(数组)
const arr = [1, 2, 3];
// 3. Function(函数)
const func = function () {};
// 4. Date
const date = new Date();
// 5. RegExp
const regex = /abc/;
// 6. Map, Set, WeakMap, WeakSet (ES6)
const map = new Map();
const set = new Set();
2. Call by Value vs Call by Reference
传值(Call by Value)vs 传址(Call by Reference)
传值(Call by Value)- 原始类型
行为:复制值本身,修改副本不影响原值。
// 原始类型:传值
let a = 10;
let b = a; // 复制值
b = 20; // 修改 b
console.log(a); // 10(不受影响)
console.log(b); // 20
内存示意图:
┌─────────┐
│ Stack │
├─────────┤
│ a: 10 │ ← 独立的值
├─────────┤
│ b: 20 │ ← 独立的值(复制后修改)
└─────────┘
传址(Call by Reference)- 引用类型
行为:复制内存地址,两个变量指向同一个对象。
// 引用类型:传址
let obj1 = { name: 'John' };
let obj2 = obj1; // 复制内存地址
obj2.name = 'Jane'; // 通过 obj2 修改
console.log(obj1.name); // 'Jane'(被影响!)
console.log(obj2.name); // 'Jane'
console.log(obj1 === obj2); // true(指向同一个对象)
内存示意图:
┌─────────┐ ┌──────────────────┐
│ Stack │ │ Heap │
├─────────┤ ├──────────────────┤
│ obj1 ───┼───────────────────>│ { name: 'Jane' } │
├─────────┤ │ │
│ obj2 ───┼───────────────────>│ (同一个对象) │
└─────────┘ └──────────────────┘
3. Common Quiz Questions
常见测验题
题目 1:原始类型的传递
function changeValue(x) {
x = 100;
console.log('函数内 x:', x);
}
let num = 50;
changeValue(num);
console.log('函数外 num:', num);
点击查看答案
// 函数内 x: 100
// 函数外 num: 50
解释:
num是原始类型(Number)- 传入函数时复制值,
x和num是两个独立的变量 - 修改
x不会影响num
// 执行流程
let num = 50; // Stack: num = 50
changeValue(num); // Stack: x = 50(复制)
x = 100; // Stack: x = 100(只修改 x)
console.log(num); // Stack: num = 50(不受影响)
题目 2:引用类型的传递
function changeObject(obj) {
obj.name = 'Changed';
console.log('函数内 obj.name:', obj.name);
}
let person = { name: 'Original' };
changeObject(person);
console.log('函数外 person.name:', person.name);
点击查看答案
// 函数内 obj.name: Changed
// 函数外 person.name: Changed
解释:
person是引用类型(Object)- 传入函数时复制内存地址
obj和person指向同一个对象- 通过
obj修改对象内容,person也会被影响
// 内存示意
let person = { name: 'Original' }; // Heap: 创建对象 @0x001
changeObject(person); // Stack: obj = @0x001(复制地址)
obj.name = 'Changed'; // Heap: @0x001.name = 'Changed'
console.log(person.name); // Heap: @0x001.name(同一个对象)
题目 3:重新赋值 vs 修改属性
function test1(obj) {
obj.name = 'Modified'; // 修改属性
}
function test2(obj) {
obj = { name: 'New Object' }; // 重新赋值
}
let person = { name: 'Original' };
test1(person);
console.log('A:', person.name);
test2(person);
console.log('B:', person.name);
点击查看答案
// A: Modified
// B: Modified(不是 'New Object'!)
解释:
test1:修改属性
function test1(obj) {
obj.name = 'Modified'; // ✅ 修改原对象的属性
}
// person 和 obj 指向同一个对象,所以会被修改
test2:重新赋值
function test2(obj) {
obj = { name: 'New Object' }; // ❌ 只改变 obj 的指向
}
// obj 现在指向新对象,但 person 仍指向原对象
内存示意图:
// test1 之前
person ────> { name: 'Original' }
obj ────> { name: 'Original' } (同一个)
// test1 之后
person ────> { name: 'Modified' }
obj ────> { name: 'Modified' } (同一个)
// test2 执行
person ────> { name: 'Modified' } (不变)
obj ────> { name: 'New Object' } (新对象)
// test2 结束后
person ────> { name: 'Modified' } (仍然不变)
// obj 被销毁,新对象被垃圾回收
题目 4:数组的传递
function modifyArray(arr) {
arr.push(4);
console.log('1:', arr);
}
function reassignArray(arr) {
arr = [5, 6, 7];
console.log('2:', arr);
}
let numbers = [1, 2, 3];
modifyArray(numbers);
console.log('3:', numbers);
reassignArray(numbers);
console.log('4:', numbers);
点击查看答案
// 1: [1, 2, 3, 4]
// 3: [1, 2, 3, 4]
// 2: [5, 6, 7]
// 4: [1, 2, 3, 4]
解释:
modifyArray:修改原数组内容,numbers被影响reassignArray:只改变参数的指向,numbers不受影响
题目 5:比较运算
// 原始类型比较
let a = 10;
let b = 10;
console.log('A:', a === b);
// 引用类型比较
let obj1 = { value: 10 };
let obj2 = { value: 10 };
let obj3 = obj1;
console.log('B:', obj1 === obj2);
console.log('C:', obj1 === obj3);
点击查看答案
// A: true
// B: false
// C: true
解释:
原始类型:比较值
10 === 10; // true(值相同)
引用类型:比较内存地址
obj1 === obj2; // false(不同对象,不同地址)
obj1 === obj3; // true(指向同一个对象)
内存示意:
obj1 ────> @0x001: { value: 10 }
obj2 ────> @0x002: { value: 10 } (内容相同但地址不同)
obj3 ────> @0x001: { value: 10 } (与 obj1 相同地址)
4. Shallow Copy vs Deep Copy
浅拷贝 vs 深拷贝
浅拷贝(Shallow Copy)
定义:只复制第一层,嵌套对象仍然是引用。
方法 1:展开运算符(Spread Operator)
const original = {
name: 'John',
address: { city: 'Taipei' },
};
const copy = { ...original };
// 修改第一层 :不影响原对象
copy.name = 'Jane';
console.log(original.name); // 'John'(不受影响)
// 修改嵌套对象:影响原对象!
copy.address.city = 'Kaohsiung';
console.log(original.address.city); // 'Kaohsiung'(被影响!)
方法 2:Object.assign()
const original = { name: 'John', age: 30 };
const copy = Object.assign({}, original);
copy.name = 'Jane';
console.log(original.name); // 'John'(不受影响)
方法 3:数组的浅拷贝
const arr1 = [1, 2, 3];
// 方法 1:展开运算符
const arr2 = [...arr1];
// 方法 2:slice()
const arr3 = arr1.slice();
// 方法 3:Array.from()
const arr4 = Array.from(arr1);
arr2[0] = 999;
console.log(arr1[0]); // 1(不受影响)
深拷贝(Deep Copy)
定义:完全复制所有层级,包括嵌套对象。
方法 1:JSON.parse + JSON.stringify(最常用)
const original = {
name: 'John',
address: { city: 'Taipei' },
hobbies: ['reading', 'gaming'],
};
const copy = JSON.parse(JSON.stringify(original));
// 修改嵌套对象:不影响原对象
copy.address.city = 'Kaohsiung';
console.log(original.address.city); // 'Taipei'(不受影响)
copy.hobbies.push('coding');
console.log(original.hobbies); // ['reading', 'gaming'](不受影响)
限制:
const obj = {
date: new Date(), // ❌ 会变成字符串
func: () => {}, // ❌ 会被忽略
undef: undefined, // ❌ 会被忽略
symbol: Symbol('test'), // ❌ 会被忽略
regexp: /abc/, // ❌ 会变成 {}
circular: null, // ❌ 循环引用会报错
};
obj.circular = obj; // 循环引用
JSON.parse(JSON.stringify(obj)); // 会出错或丢失数据
方法 2:structuredClone()(现代浏览器)
const original = {
name: 'John',
address: { city: 'Taipei' },
date: new Date(),
};
const copy = structuredClone(original);
// 可以正确复制 Date 等特殊对象
console.log(copy.date instanceof Date); // true
优点:
- ✅ 支持 Date、RegExp、Map、Set 等
- ✅ 支持循环引用
- ✅ 性能较好
限制:
- ❌ 不支持函数
- ❌ 不支持 Symbol
方法 3:递归实现深拷贝
function deepClone(obj) {
// 处理 null 和非对象
if (obj === null || typeof obj !== 'object') {
return obj;
}
// 处理数组
if (Array.isArray(obj)) {
return obj.map((item) => deepClone(item));
}
// 处理 Date
if (obj instanceof Date) {
return new Date(obj);
}
// 处理 RegExp
if (obj instanceof RegExp) {
return new RegExp(obj);
}
// 处理对象
const cloned = {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
cloned[key] = deepClone(obj[key]);
}
}
return cloned;
}
// 使用示例
const original = {
name: 'John',
address: { city: 'Taipei' },
hobbies: ['reading'],
date: new Date(),
};
const copy = deepClone(original);
copy.address.city = 'Kaohsiung';
console.log(original.address.city); // 'Taipei'(不受影响)
方法 4:使用 Lodash
import _ from 'lodash';
const original = {
name: 'John',
address: { city: 'Taipei' },
};
const copy = _.cloneDeep(original);