如何正确克隆 JavaScript 对象
技术背景
在 JavaScript 中,对象是引用类型。当我们将一个对象赋值给另一个变量时,实际上是复制了对象的引用,而不是对象本身。这意味着对新变量的修改会影响原始对象。在某些情况下,我们需要创建一个对象的副本,使得对副本的修改不会影响原始对象,这就需要进行对象克隆。
实现步骤
浅拷贝
浅拷贝只复制对象的一层属性,如果对象的属性是引用类型,则只复制引用,而不是对象本身。
1. 使用 Object.assign()
const original = { a: 1, b: { c: 2 } };
const shallowClone = Object.assign({}, original);
2. 使用扩展运算符(Spread Operator)
const original = { a: 1, b: { c: 2 } };
const shallowClone = { ...original };
深拷贝
深拷贝会递归地复制对象的所有属性,包括嵌套对象,从而创建一个完全独立的对象副本。
1. 使用 JSON.parse(JSON.stringify())
const original = { a: 1, b: { c: 2 } };
const deepClone = JSON.parse(JSON.stringify(original));
注意:该方法有局限性,它会忽略 undefined
、function
、Symbol
等属性,并且不能处理循环引用的对象。
2. 自定义递归函数
function clone(obj) {
if (null == obj || "object" != typeof obj) return obj;
if (obj instanceof Date) {
const copy = new Date();
copy.setTime(obj.getTime());
return copy;
}
if (obj instanceof Array) {
const copy = [];
for (let i = 0, len = obj.length; i < len; i++) {
copy[i] = clone(obj[i]);
}
return copy;
}
if (obj instanceof Object) {
const copy = {};
for (const attr in obj) {
if (obj.hasOwnProperty(attr)) copy[attr] = clone(obj[attr]);
}
return copy;
}
throw new Error("Unable to copy obj! Its type isn't supported.");
}
const original = { a: 1, b: { c: 2 } };
const deepClone = clone(original);
3. 使用 structuredClone()
const original = { a: 1, b: { c: 2 } };
const deepClone = structuredClone(original);
注意:structuredClone()
是一个新的 JavaScript 标准,浏览器兼容性需要检查。
使用第三方库
1. jQuery
const original = { a: 1, b: { c: 2 } };
// 浅拷贝
const shallowClone = jQuery.extend({}, original);
// 深拷贝
const deepClone = jQuery.extend(true, {}, original);
2. Lodash
const _ = require('lodash');
const original = { a: 1, b: { c: 2 } };
// 浅拷贝
const shallowClone = _.clone(original);
// 深拷贝
const deepClone = _.cloneDeep(original);
最佳实践
- 浅拷贝:如果对象只有一层属性或者不需要对嵌套对象进行独立修改,使用
Object.assign()
或扩展运算符进行浅拷贝,它们简洁高效。 - 深拷贝:
- 如果对象不包含
undefined
、function
、Symbol
等特殊属性,且没有循环引用,可以使用JSON.parse(JSON.stringify())
。 - 如果需要处理复杂对象,包括日期、数组等,自定义递归函数是一个不错的选择。
- 如果浏览器支持,
structuredClone()
是最简单的深拷贝方法。 - 如果项目中已经使用了 jQuery 或 Lodash 等库,使用它们提供的克隆方法更方便。
- 如果对象不包含
常见问题
循环引用问题
使用 JSON.parse(JSON.stringify())
和简单的递归函数会遇到循环引用问题,导致错误。可以使用带有缓存的递归函数来解决,例如:
function cloneWithCache(obj) {
const cache = new WeakMap();
function copy(object) {
if (typeof object !== 'object' || object === null) return object;
if (cache.has(object)) return cache.get(object);
let result;
if (object instanceof Date) {
result = new Date(object.getTime());
} else if (object instanceof Array) {
result = [];
for (const item of object) {
result.push(copy(item));
}
} else {
result = {};
for (const key in object) {
if (object.hasOwnProperty(key)) {
result[key] = copy(object[key]);
}
}
}
cache.set(object, result);
return result;
}
return copy(obj);
}
const circularObj = {};
circularObj.self = circularObj;
const clonedCircularObj = cloneWithCache(circularObj);
性能问题
JSON.parse(JSON.stringify())
涉及字符串的序列化和反序列化,性能较低,不适合处理大型对象。在性能敏感的场景下,建议使用自定义递归函数或第三方库的克隆方法。