原始值、引用值与深浅拷贝

35 阅读3分钟

1. 原始值 (Primitive Values)

JavaScript有7种原始数据类型:


// 原始值类型
let str = "hello";        // string
let num = 42;             // number  
let bool = true;          // boolean
let n = null;             // null
let u = undefined;        // undefined
let sym = Symbol('id');   // symbol
let big = 123n;           // bigint

原始值特点:

  • 存储在栈内存中
  • 按值传递/赋值
  • 不可变 (immutable)
  • 不能添加属性

2. 引用值 (Reference Values)

除了原始值之外的都是引用值:


// 引用值类型
let obj = {};             // Object
let arr = [];             // Array  
let func = function(){};  // Function
let date = new Date();    // Date
let reg = /pattern/;      // RegExp
// ... 等等

引用值特点:

  • 存储在堆内存中,变量存储的是内存地址
  • 按引用传递/赋值
  • 可变 (mutable)
  • 可以添加属性

3. 原始值 - 永远是深拷贝

原始值在赋值/传递时,总是复制值本身,不存在浅拷贝问题:


// 原始值拷贝
let a = 10;
let b = a;  // b得到a值的完整副本
a = 20;
console.log(a); // 20
console.log(b); // 10,完全独立

let str1 = "hello";
let str2 = str1; // str2得到字符串的副本
str1 = "world";
console.log(str1); // "world"
console.log(str2); // "hello",不受影响

// 原始值作为参数传递
function changeValue(x) {
  x = 100;
  return x;
}

let num = 50;
let result = changeValue(num);
console.log(num);    // 50,原值不变
console.log(result); // 100

4. 引用值 - 默认浅拷贝

引用值在直接赋值时是浅拷贝,只复制引用地址:


// 浅拷贝示例
let obj1 = {
  name: "张三",
  age: 25,
  hobbies: ["读书", "游泳"],
  address: {
    city: "北京",
    district: "朝阳区"
  }
};

// 直接赋值 = 浅拷贝
let obj2 = obj1;
obj2.name = "李四";
console.log(obj1.name); // "李四" - 原对象被影响!

// 使用扩展运算符 = 浅拷贝
let obj3 = { ...obj1 };
obj3.name = "王五";
console.log(obj1.name); // "李四" - 第一层属性独立

// 但嵌套对象仍然共享引用
obj3.address.city = "上海";
console.log(obj1.address.city); // "上海" - 嵌套对象被影响!

obj3.hobbies.push("跑步");
console.log(obj1.hobbies); // ["读书", "游泳", "跑步"] - 数组被影响!

5. 常见的浅拷贝方法



let original = {
  a: 1,
  b: { c: 2 },
  d: [3, 4]
};

// 方法1: 扩展运算符
let copy1 = { ...original };

// 方法2: Object.assign()
let copy2 = Object.assign({}, original);

// 方法3: 数组的slice()
let arr = [1, { a: 2 }, [3, 4]];
let arrCopy = arr.slice();

// 这些都是浅拷贝,嵌套对象仍然共享引用
copy1.b.c = 999;
console.log(original.b.c); // 999 - 原对象被影响

arrCopy[1].a = 888;
console.log(arr[1].a); // 888 - 原数组被影响

6. 深拷贝的实现

为了避免浅拷贝问题,需要实现深拷贝:


// 方法1: JSON序列化(有限制)
function deepCopyJSON(obj) {
  return JSON.parse(JSON.stringify(obj));
}

// 局限性:
let objWithFunction = {
  name: "张三",
  sayHello: function() { console.log("Hello"); }, // 函数会丢失
  birthday: new Date(), // Date变成字符串
  reg: /pattern/,       // 正则变成空对象
  undef: undefined      // undefined会丢失
};

let copy = deepCopyJSON(objWithFunction);
console.log(copy); 
// { name: "张三", birthday: "2024-01-01T00:00:00.000Z", reg: {} }

// 方法2: 递归深拷贝
function deepCopy(obj) {
  // 处理原始值
  if (obj === null || typeof obj !== "object") {
    return obj;
  }
  
  // 处理Date
  if (obj instanceof Date) {
    return new Date(obj.getTime());
  }
  
  // 处理Array
  if (Array.isArray(obj)) {
    return obj.map(item => deepCopy(item));
  }
  
  // 处理Object
  const cloned = {};
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      cloned[key] = deepCopy(obj[key]);
    }
  }
  return cloned;
}

// 测试深拷贝
let original2 = {
  name: "张三",
  info: {
    age: 25,
    hobbies: ["读书", "游泳"]
  }
};

let deepCopied = deepCopy(original2);
deepCopied.info.age = 30;
deepCopied.info.hobbies.push("跑步");

console.log(original2.info.age);     // 25 - 原对象不受影响
console.log(original2.info.hobbies); // ["读书", "游泳"] - 原数组不受影响

7. 实际开发中的注意事项


// 函数参数传递
function processUser(user) {
  // 错误做法:直接修改传入的对象
  user.processed = true; // 会影响原对象!
  
  // 正确做法:先深拷贝再修改
  const userCopy = deepCopy(user);
  userCopy.processed = true;
  return userCopy;
}

// React/Vue中的状态更新
// 错误做法
state.users.push(newUser); // 直接修改原数组

// 正确做法
setState({
  users: [...state.users, newUser] // 创建新数组
});

// 或者
setState({
  user: { ...state.user, name: "新名字" } // 创建新对象
});

8. 总结对比

特性原始值引用值
拷贝方式始终深拷贝默认浅拷贝
独立性完全独立可能共享引用
修改影响不影响原值浅拷贝会影响原值
内存占用较少较多(如果深拷贝)