如何拷贝一个JSON对象:从浅拷贝到深拷贝的完整指南
在JavaScript开发中,处理JSON对象(本质上是JavaScript对象)的拷贝是一项常见任务,无论是为了保护原始数据不被意外修改,还是在不同组件间传递数据副本,正确拷贝JSON对象都至关重要,简单的赋值操作并不会创建真正的拷贝,而是引用了同一个对象,本文将详细介绍如何拷贝JSON对象,涵盖浅拷贝、深拷贝的各种方法及其适用场景。
为什么不能直接赋值?
我们需要明确一个概念:在JavaScript中,对象是引用类型,当我们使用将一个对象赋值给另一个变量时,实际上只是将引用(内存地址)进行了拷贝,而不是对象本身,这意味着两个变量指向同一个对象,对任何一个变量的修改都会影响到另一个。
const original = { name: "Alice", age: 30 };
const copy = original;
copy.name = "Bob";
console.log(original.name); // 输出: Bob (original也被修改了)
我们需要使用特定的方法来创建对象的真正拷贝。
浅拷贝 (Shallow Copy)
浅拷贝只拷贝对象的第一层属性,如果属性的值是基本类型(如string, number, boolean),则直接拷贝其值;如果属性的值是对象(包括数组、函数、其他对象等),则拷贝的是其引用(内存地址),而不是对象本身。
使用Object.assign()方法
Object.assign()方法用于将所有可枚举的自有属性从一个或多个源对象复制到目标对象,我们可以创建一个空对象作为目标对象,从而实现浅拷贝。
const original = { name: "Alice", age: 30, address: { city: "New York" } };
const copy = Object.assign({}, original);
copy.name = "Bob";
copy.address.city = "Boston";
console.log(copy.name); // 输出: Bob
console.log(original.name); // 输出: Alice (基本类型未被影响)
console.log(copy.address.city); // 输出: Boston
console.log(original.address.city); // 输出: Boston (嵌套对象被影响,因为是引用拷贝)
使用展开语法 (Spread Syntax)
ES6引入的展开语法提供了一种更简洁的方式进行浅拷贝,特别是对于对象和数组。
const original = { name: "Alice", age: 30, address: { city: "New York" } };
const copy = { ...original };
copy.name = "Bob";
copy.address.city = "Boston";
console.log(copy.name); // 输出: Bob
console.log(original.name); // 输出: Alice
console.log(copy.address.city); // 输出: Boston
console.log(original.address.city); // 输出: Boston (嵌套对象被影响)
使用Array.prototype.slice()或Array.prototype.concat()(针对数组)
虽然这些是数组方法,但也可以用来创建数组的浅拷贝:
const originalArray = [1, { a: 2 }, [3, 4]];
const copyArray = originalArray.slice(); // 或 [...originalArray] 或 originalArray.concat()
copyArray[0] = 100;
copyArray[1].a = 200;
copyArray[2][0] = 300;
console.log(copyArray[0]); // 输出: 100
console.log(originalArray[0]); // 输出: 1 (基本类型未被影响)
console.log(copyArray[1].a); // 输出: 200
console.log(originalArray[1].a); // 输出: 200 (嵌套对象被影响)
console.log(copyArray[2][0]); // 输出: 300
console.log(originalArray[2][0]); // 输出: 300 (嵌套数组被影响)
深拷贝 (Deep Copy)
深拷贝会递归地拷贝对象的所有层级,确保拷贝后的对象与原始对象完全独立,修改拷贝对象不会对原始对象造成任何影响。
使用JSON.stringify()和JSON.parse()
这是一种常见且简单的方法,通过将对象序列化为JSON字符串,然后再解析为新的对象来实现深拷贝。
const original = { name: "Alice", age: 30, address: { city: "New York" } };
const copy = JSON.parse(JSON.stringify(original));
copy.name = "Bob";
copy.address.city = "Boston";
console.log(copy.name); // 输出: Bob
console.log(original.name); // 输出: Alice
console.log(copy.address.city); // 输出: Boston
console.log(original.address.city); // 输出: New York (嵌套对象未被影响)
优点:
- 简单易用,代码简洁。
- 对于纯数据对象(不包含函数、Symbol、undefined、循环引用等)非常有效。
缺点:
- 会丢失函数:对象中的方法(函数)在序列化时会被丢弃。
- 会丢失Symbol:Symbol类型的属性会被忽略。
- 会丢失undefined:值为
undefined的属性会被忽略。 - 不能处理循环引用:如果对象中存在循环引用(
obj.a = obj;),会抛出错误TypeError: Converting circular structure to JSON。 - 不能处理Date对象:Date对象会被转换为字符串,而不是Date对象。
- 不能处理正则表达式:正则表达式会被转换为空对象。
使用递归函数实现深拷贝
为了克服JSON.stringify()方法的局限性,我们可以编写自定义的递归深拷贝函数。
function deepClone(obj, hash = new WeakMap()) {
// 处理 null 或非对象
if (obj === null || typeof obj !== 'object') {
return obj;
}
// 处理 Date 对象
if (obj instanceof Date) {
return new Date(obj);
}
// 处理 RegExp 对象
if (obj instanceof RegExp) {
return new RegExp(obj);
}
// 处理循环引用
if (hash.has(obj)) {
return hash.get(obj);
}
// 获取对象所有的属性描述符(包括可枚举和不可枚举的)
const allDesc = Object.getOwnPropertyDescriptors(obj);
// 创建新对象,并继承原型链
const cloneObj = Object.create(Object.getPrototypeOf(obj), allDesc);
// 将当前对象存入 WeakMap,用于循环引用检测
hash.set(obj, cloneObj);
// 递归拷贝所有属性(包括Symbol属性)
for (let key of Reflect.ownKeys(obj)) {
cloneObj[key] = deepClone(obj[key], hash);
}
return cloneObj;
}
// 示例
const original = {
name: "Alice",
age: 30,
address: { city: "New York" },
sayHello: function() { console.log("Hello"); },
[Symbol("id")]: 123,
birthday: new Date(),
regex: /test/g
};
original.self = original; // 循环引用
const copy = deepClone(original);
copy.name = "Bob";
copy.address.city = "Boston";
copy.sayHello(); // 输出: Hello (方法被保留)
console.log(copy[Symbol("id")]); // 输出: 123 (Symbol属性被保留)
console.log(copy.birthday instanceof Date); // true (Date对象被正确拷贝)
console.log(copy.regex instanceof RegExp); // true (正则表达式被正确拷贝)
console.log(copy.self === copy); // true (循环引用被正确处理)
console.log(original.self === original); // true (原始对象的循环引用依然存在)
优点:
- 可以处理函数、Symbol、Date、RegExp等复杂类型。
- 可以正确处理循环引用。
- 保留对象的原型链和不可枚举属性。
缺点:
- 实现相对复杂。
- 对于某些特殊对象(如Map、Set、Buffer等)可能需要额外处理。
- 性能可能不如
JSON.stringify()方法,尤其是对于非常深的对象结构。
使用第三方库
在实际项目中,使用成熟的第三方库通常是更可靠和高效的选择,这些库已经考虑了各种边界情况和性能优化。
-
Lodash:
_.cloneDeep()方法提供了非常强大和可靠的深拷贝功能。const _ = require('lodash'); const original = { name: "Alice", address: { city: "New York" } }; const copy = _.cloneDeep(original); copy.address.city = "Boston"; console.log(original.address.city); // 输出: New York -
Rambda:
R.clone()是Rambda库中的深拷贝方法,轻量级且功能完善。import { clone } from 'rambda'; const original = { name: "Alice", address: { city: "New York" } }; const copy = clone(original); copy.address.city = "Boston"; console.log(original.address.city); // 输出: New York ``



还没有评论,来说两句吧...