在 JavaScript 开发里,对象复制极为常见。不管是传递数据、管理状态,还是处理函数参数,我们常需复制对象,避免直接修改原始对象,或在不同场景复用相同结构数据。浅拷贝与深拷贝是实现对象复制的两种关键手段,理解并合理运用它们,对编写优质代码意义重大。
浅拷贝(Shallow Copy)
定义与特点
浅拷贝仅复制对象的第一层属性。所谓第一层属性,指的是直接隶属于该对象的属性,而非对象内部嵌套对象的属性。
在浅拷贝过程中,针对不同数据类型,处理方式和结果各异:
- 基础数据类型 :在 JavaScript 里,数字(
number)、字符串(string)、布尔值(boolean)、null以及undefined都属于基础数据类型。浅拷贝时,针对这些类型,会在新对象中生成全新且独立的值。 - 引用数据类型 :对象(
object)和数组(array)这类引用数据类型,在内存中存储的是指向实际数据存储位置的引用地址。- 浅拷贝对于引用数据类型,仅仅复制该引用地址,并非实际数据。这就致使新对象和原对象中的引用类型属性指向同一块内存区域,进而对其中一个对象的引用类型属性进行修改,另一个对象也会受到影响。
以如下对象为例:
let obj = {
name: 'John',
age: 30,
address: {
street: '123 Main St',
city: 'Anytown'
}
};
-
在
obj中,name、age和address属于第一层属性,它们直接存在于obj中。而street和city是嵌套在address对象内的属性,不属于第一层属性。 -
浅拷贝时,仅会复制
name、age和address这一层属性,其中对于基础数据类型的name和age,会创建新值;对于引用类型的address,则复制其引用地址,使得新对象和原对象中的address属性指向同一内存区域 。
适用场景
当我们仅关注对象的第一层属性,且确定引用类型属性在后续操作中不会被修改,又或是期望新对象与原对象在某些引用类型属性上保持同步变化,浅拷贝便是不错的选择。
在简单的数据展示场景中,这种情况尤为适用。比如展示用户的基本信息等。
对象浅拷贝
- Object.assign()
Object.assign()是 JavaScript 内置的用于对象合并和浅拷贝的方法。它可以将一个或多个源对象的属性复制到目标对象上。- 语法为
Object.assign(target, ...sources),其中target是目标对象,sources是一个或多个源对象。
const source = { a: 1, b: { c: 2 } };
const shallowCopiedByAssign = Object.assign({}, source);
// 修改浅拷贝对象的基础数据类型属性
shallowCopiedByAssign.a = 10;
// 修改浅拷贝对象的引用数据类型属性
shallowCopiedByAssign.b.c = 20;
console.log(source.a); // 输出: 1
console.log(shallowCopiedByAssign.a); // 输出: 10
console.log(source.b.c); // 输出: 20
console.log(shallowCopiedByAssign.b.c); // 输出: 20
- 上述代码创建了一个对象
source,然后使用Object.assign()方法对其进行浅拷贝,得到一个新对象shallowCopiedByAssign。 - 由于是浅拷贝,对于
source中的基础数据类型属性(如a),shallowCopiedByAssign会拥有一个独立的副本; - 而对于引用数据类型属性(如
b),shallowCopiedByAssign和source中的b会指向同一个内存地址。这就意味着,修改shallowCopiedByAssign.b会影响到source.b,反之亦然。
- 扩展运算符 ...
扩展运算符...能够将可迭代对象(如数组、字符串等)展开为单个元素,也能在对象上下文中把对象的可枚举属性复制到新对象中,实现对象的浅拷贝。- 一般来说,直接在对象字面量中定义的属性,或者通过
Object.defineProperty()方法定义且没有将enumerable属性设置为false的属性,都是可枚举属性。
- 一般来说,直接在对象字面量中定义的属性,或者通过
- 简单来说,当用于对象时,它会创建一个新对象,新对象拥有原对象所有可枚举属性的副本。
const originalObject = { x: 3, y: { z: 4 } };
const shallowCopiedObject = {...originalObject };
- Object.create () 结合 Object.keys ()
Object.create()方法用于创建一个新对象,它允许使用现有的对象来提供新创建对象的__proto__,也就是新对象的原型会指向该现有对象。- 而
Object.keys()方法会返回一个由给定对象的所有可枚举属性组成的数组。将这两个方法结合使用,能够实现对象的浅拷贝。
t source = { a: 1, b: { c: 2 } };
// 创建新对象,设置其原型与 source 相同
const shallowCopied = Object.create(Object.getPrototypeOf(source));
// 获取 source 可枚举属性名
const keys = Object.keys(source);
// 遍历属性名,复制属性值到新对象
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
shallowCopied[key] = source[key];
}
shallowCopied.b.c = 3;
// 因浅拷贝,原对象对应属性值也改变
console.log(source.b.c);
数组浅拷贝
- 扩展运算符 ...
在创建新数组时使用扩展运算符,可以实现浅拷贝,它会复制数组的元素。
const originalArray = [1, 2, [3, 4]];
const shallowCopiedArray = [...originalArray];
- Array.prototype.slice()
slice()方法返回一个新的数组对象,这一对象由该方法的开始和结束(不包括结束)选择的原数组的一部分浅拷贝组成。如果不指定begin和end,则会浅拷贝整个数组。
const arr = [1, 2, [3, 4]];
const shallowCopiedSlice = arr.slice();
- Array.prototype.concat()
concat()方法用于合并两个或多个数组,它会返回一个新的数组,该数组是由原数组和传入的数组或值合并而成。如果不传入任何参数,它会返回原数组的一个浅拷贝。
const originalArray = [1, 2, [3, 4]];
const shallowCopiedArray = originalArray.concat();
- Array.from()
Array.from()方法从一个类似数组或可迭代对象创建一个新的、浅拷贝的数组实例。
const originalArray = [1, 2, [3, 4]];
const shallowCopiedArray = Array.from(originalArray);
潜在问题
由于浅拷贝对于引用数据类型只是复制引用地址,当原对象和浅拷贝对象共享的引用类型属性被修改时,会影响到彼此。
在业务逻辑里,要是不小心修改了浅拷贝对象中引用类型属性的值,原对象中对应的属性值也会跟着改变,反之亦然。这或许会造成数据混乱,让程序的运行结果不符合预期。
深拷贝(Deep Copy)
定义与特点
- 深拷贝会递归地复制对象的所有层级属性,包括嵌套的对象和数组等。
- 这一过程会构建一个全新的对象,新对象的每一层属性均为独立副本,与原对象不存在任何引用关联。因此,对新对象属性的任何修改,都不会波及原对象,反之亦然,新旧对象在数据层面实现了彻底的隔离。
适用场景
当我们需要确保新对象和原对象在任何情况下都完全独立,互不干扰时,深拷贝是必须的。
- 比如在一些涉及复杂数据结构且数据安全性要求较高的场景,如金融数据处理、复杂游戏场景数据管理等,深拷贝能保证数据的完整性和独立性。
实现方式
- JSON.parse (JSON.stringify ()) :
工作原理
此方法借助了 JavaScript 内建的 JSON.stringify() 和 JSON.parse() 函数。
-
JSON.stringify():该函数会把 JavaScript 对象转化为 JSON 字符串。- JSON 字符串是一种基于 JavaScript 语法的文本格式,用于表示结构化数据。它以字符串的形式存储和传输数据:
- JSON 字符串中的键必须是字符串,并且必须使用双引号括起来。
- 值可以是对象,数组,字符串,数字,布尔值,null 数据类型。
- 整个 JSON 字符串必须是一个有效的 JSON 数据结构,通常是一个对象或数组。
- 在转换过程中,对象的结构和属性值都会被转换为符合 JSON 格式的字符串表示。·例如,对于对象
{ a: 1, b: { c: 2 } },JSON.stringify()会将其转换为字符串"{"a":1,"b":{"c":2}}"。
- JSON 字符串是一种基于 JavaScript 语法的文本格式,用于表示结构化数据。它以字符串的形式存储和传输数据:
-
JSON.parse():该函数能把 JSON 字符串解析为 JavaScript 对象。- 当传入
JSON.stringify()生成的字符串时,它会创建一个新的 JavaScript 对象,这个新对象在结构和属性值上与原对象相同,但在内存中是独立的。
- 当传入
-
这种方法先将对象转换为 JSON 字符串,再将 JSON 字符串解析为新的对象。
-
由于 JSON 格式的限制,它无法处理函数、正则表达式、循环引用等特殊情况,只能处理包含基础数据类型和普通对象、数组的简单结构。
const original = { a: 1, b: { c: 2 } };
// 使用 JSON.stringify 将原始对象转换为 JSON 字符串,再用 JSON.parse 将字符串解析为新对象,实现深拷贝
const deepCopyByJSON = JSON.parse(JSON.stringify(original));
deepCopyByJSON.b.c = 3;
// 原始对象中嵌套对象的属性 c 的值,由于是深拷贝,原始对象不受影响,结果为 2
console.log(original.b.c);
// 深拷贝对象中嵌套对象的属性 c 的值,已被修改为 3
console.log(deepCopyByJSON.b.c);
- 递归实现 :通过编写递归函数,遍历对象的每一层属性,对于基础数据类型直接复制,对于引用数据类型则继续递归调用深拷贝函数,直到所有层级的属性都被复制,从而实现深拷贝。
function deepClone(obj) {
if (typeof obj!== 'object' || obj === null) {
return obj;
}
let clone = Array.isArray(obj)? [] : {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
clone[key] = deepClone(obj[key]);
}
}
return clone;
}
- 第三方库(lodash.cloneDeep) :lodash 库中的
cloneDeep方法是一个专门用于深拷贝的函数,它能够处理各种复杂的对象结构,包括函数、循环引用等,并且性能和兼容性都较好,是在实际开发中进行深拷贝的常用选择。首先需要引入 lodash 库,然后使用_.cloneDeep()方法。
import cloneDeep from 'lodash/cloneDeep';
import cloneDeep from 'lodash/cloneDeep';
const original = { a: 1, b: { c: 2 } };
const deepCopyByLodash = cloneDeep(original);
手写实现
手写浅拷贝
function shallowClone(obj) {
if (typeof obj!== 'object' || obj === null) {
return obj;
}
let clone = Array.isArray(obj)? [] : {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
clone[key] = obj[key];
}
}
return clone;
}
手写深拷贝(递归版)
function deepClone(obj) {
if (typeof obj!== 'object' || obj === null) {
return obj;
}
let clone = Array.isArray(obj)? [] : {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
clone[key] = deepClone(obj[key]);
}
}
return clone;
}
手写深拷贝(解决循环引用)
function deepCloneWithCircular(obj, map = new WeakMap()) {
if (typeof obj!== 'object' || obj === null) {
return obj;
}
if (map.has(obj)) {
return map.get(obj);
}
let clone = Array.isArray(obj)? [] : {};
map.set(obj, clone);
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
clone[key] = deepCloneWithCircular(obj[key], map);
}
}
return clone;
}
核心逻辑是利用递归遍历对象属性进行复制,借助 WeakMap 避免循环引用导致的无限递归。
- 若
WeakMap里已有该对象,说明是循环引用,直接返回之前克隆好的对象。 - 把原始对象和克隆对象存入
WeakMap,方便后续判断。
总结
如何选择拷贝方式?
- 如果对象结构简单,且只需要复制第一层属性,或者希望新对象和原对象在某些引用类型属性上保持同步变化,浅拷贝是一个高效的选择。
- 当对象结构复杂,包含多层嵌套,并且需要确保新对象和原对象完全独立,互不干扰时,深拷贝是必须的。
- 在选择深拷贝方式时,如果对象结构简单,不包含特殊数据类型(如函数、循环引用、Symbol 等),
JSON.parse(JSON.stringify())是一个简单快捷的方法; - 如果对象结构复杂,包含特殊数据类型,建议使用第三方库(如 lodash 的
cloneDeep)或者自己实现一个能处理特殊数据类型的递归深拷贝函数。
- 在选择深拷贝方式时,如果对象结构简单,不包含特殊数据类型(如函数、循环引用、Symbol 等),
性能对比
- 浅拷贝的性能通常优于深拷贝,因为浅拷贝只复制一层属性,而深拷贝需要递归遍历所有层级的属性。
- 在处理大对象时,深拷贝可能会消耗大量的内存和时间。
- 因此,在满足需求的前提下,应优先考虑浅拷贝。但如果必须保证数据的独立性,即使性能有所损耗,也需要使用深拷贝。
推荐方法
- 在实际开发中,对于浅拷贝,建议优先使用扩展运算符,因为其语法简洁明了。
- 对于深拷贝,如果项目中已经引入了 lodash 库,使用
_.cloneDeep()是最方便可靠的选择。 - 如果项目对体积敏感,不希望引入额外的库,可以根据实际需求,自己实现一个能处理特殊数据类型的深拷贝函数。