前言
- 在编程世界中,对象和数据结构的复制是一个常见而重要的操作。
- 然而,拷贝并不总是简单的,因为它可能涉及到引用和嵌套的对象。
- 在这个背景下,我们将探讨两种主要的拷贝方式:浅拷贝和深拷贝。
- 理解这两种拷贝方式的内涵以及它们在不同场景下的用途,将有助于编写出更加强大和可靠的代码。
- 在下文中,我们将深入探讨如何实现浅拷贝和深拷贝,以及它们的实际应用。
浅拷贝
- 浅拷贝通常用于复制对象、数组等复杂的数据结构,但只复制它们的外部结构和第一层元素。
- 对于原始数据结构包含的基本数据类型,浅拷贝会直接复制出一份新数据, 新老数据之间不会存在任何关系。
- 但是如果原始数据结构包含嵌套的对象或数组,浅拷贝会复制嵌套元素的引用,导致新的数据结构和原始数据结构之间共享该嵌套元素。
- 下面提供了两种方式实现浅拷贝:
*- 使用 ES6 实现浅拷贝(附带图解)
-
- 使用 for...in + hasOwnProperty() 实现浅拷贝
使用 ES6 实现浅拷贝
代码实现
js
function shallowClone(target) {
// 说明是数组
if (target instanceof Array) {
// 方式1:
return [...target];
// 方式2:
// return target.slice()
// 方式3:
// return [].concat(target)
// 方式4:Array.from() 就是将一个类数组转为真正的数组
// return Array.from(target)
// 方式5:
// return target.reduce((pre, item) => {
// pre.push(item);
// return pre;
// }, []);
// ......
} else if (target !== null && typeof target === "object") {
// 说明只能是个对象或者数组(也不可能是函数),但是对于数组,上一个分支已经走了,所以这里只可能是对象
return {
...target,
};
} else {
// 说明不是对象也不是数组
return target;
}
}
let target = {
name: "zsy",
address: {
x: 100,
y: 200,
},
};
let result = shallowClone(target); // 最终拿到的result对象为这样:{name:"zsy", address: 'address地址值', }
console.log(result === target); // false
// 1. 修改的只是基本数据类型
// target.name = 'peiqi'
// console.log(target.name) // peiqi
// console.log(result.name) // zsy
// 2. 修改了引用数据类型的数据
target.address.x = 5000;
console.log(target.address.x); // 5000
console.log(result.address.x); // 5000
console.log(result.address === target.address); // true
图解
- 下面我们通过图解的形式对上面的代码进行分析:
原始对象
target
, 它包含了两个元素, 一个是基本数据类型name
, 另一个是对象address
:
当使用浅拷贝方法创建了新对象result
时, 对于原始对象中基本数据类型, 则是复制一份。而对于引用数据类型则会复制其引用:
- 如果通过拷贝后的新对象去修改其子元素
address
中的数据时, 会导致原始对象的address
也会被修改。 - 这就是浅拷贝的特点, 新对象和原始对象共享其子元素中的引用数据类型。如果需要避免这种共享, 就需要使用深拷贝。
使用 for...in + hasOwnProperty() 实现浅拷贝
补充 hasOwnProperty() 基本用法
-
在实现浅拷贝之前, 我们先温习下
hasOwnProperty()
的基本用法。 -
在 JavaScript 中,hasOwnProperty 是一个对象的方法,用于检查
对象自身
是否具有指定的属性(不包括从原型链继承的属性)
。它返回一个布尔值,指示对象自身是否拥有该属性。 -
使用 hasOwnProperty 方法可以帮助你确定一个属性是对象自身拥有的,还是继承自其原型链。这在编程中非常有用,特别是当你需要遍历对象的属性并只处理对象自身的属性时。
-
以下是使用 hasOwnProperty 方法的示例:
js
const person = {
name: "peiqi",
age: 22,
};
console.log(person.hasOwnProperty("name")); // true
console.log(person.hasOwnProperty("city")); // false
- 在 JavaScript 中,数组是一种特殊的对象,因此数组实例也继承了对象的属性和方法。包括 hasOwnProperty 方法在内的大多数对象原型上的方法也可以在数组实例上使用。
- 然而,需要注意的是,hasOwnProperty 方法只用于检查对象自身是否具有属性,而不会查找原型链。在数组实例上调用 hasOwnProperty 方法会检查数组实例自身是否具有指定的属性,但不会检查数组的元素。
- 以下是一个示例,演示了在数组实例上使用 hasOwnProperty 方法:
js
const arr = [1, 2, 3];
console.log(arr.hasOwnProperty(0)); // true,数组自身具有索引为 0 的属性
console.log(arr.hasOwnProperty(1)); // true,数组自身具有索引为 1 的属性
console.log(arr.hasOwnProperty(3)); // false,数组自身没有索引为 3 的属性
console.log(arr.hasOwnProperty("length")); // true,数组自身具有 length 属性
console.log(arr.hasOwnProperty("toString")); // false,toString 是从原型链继承的方法
代码实现
下面我们就使用 for...in
实现浅拷贝, 代码如下:
js
function shallowClone(target) {
// 说明对象或者是数组
if (target !== null && typeof target === "object") {
const cloneTarget = target instanceof Array ? [] : {};
for (const key in target) {
// for...in 会遍历【自身的】和【原型上】的所有属性, 如果是遍历对象,则 key 是属性,如果是数组,则 key 是索引
if (target.hasOwnProperty(key)) {
cloneTarget[key] = target[key];
}
}
return cloneTarget;
} else {
return target;
}
}
深拷贝
- 深拷贝会递归地复制原始数据结构中的所有元素,包括嵌套的对象、数组等。
- 深拷贝会复制元素的内容,而不是仅复制元素的引用,因此新的数据结构和原始数据结构是完全独立的,彼此之间不会共享任何元素。
- 由于深拷贝会递归地复制所有的嵌套元素,因此在处理大型数据结构时可能会导致性能问题。
- 下面提供了两种方式实现深拷贝:
*- 使用
JSON.stringify()
实现深拷贝
-
- 使用递归实现深拷贝(附带优化版)
- 使用
使用 JSON.stringify() 实现深拷贝
正常使用
我们先来看下基本的实现
js
let target = {
name: "zsy",
address: {
x: 100,
y: 200,
},
};
// 调用 JSON.stringify 方法将对象转为 JSON 字符串
let str = JSON.stringify(target);
console.log(str); // str为: "{"name":"peiqi","address":{"x":100,"y":200}}"
// 调用 JSON.parse 方法就是将 JSON 字符串还原为对象
let result = JSON.parse(str);
target.address.x = 300;
console.log(target); // { name: 'zsy', address: { x: 300, y: 200 } }
console.log(result); // { name: 'zsy', address: { x: 100, y: 200 } }
无法解析特殊的值
但是注意:JSON.stringify 无法解析特殊的值,比如 undefined,函数等,拷贝后会发生丢失
js
let target = {
name: "zsy",
address: {
x: 100,
y: 200,
},
fn: function () {},
un: undefined,
};
// 调用JSON.stringify方法将对象转为字符串
let str = JSON.stringify(target);
let result = JSON.parse(str);
target.address.x = 300;
console.log(target); // { name: 'zsy', address: { x: 300, y: 200 }, fn: [Function: fn], un: undefined }
console.log(result); // { name: 'zsy', address: { x: 100, y: 200 } }
从上面的代码中, 可以看出拷贝后的 result
对象丢失了 fn
属性和 un
属性。
无法处理循环引用
除此之外, JSON.stringify 还无法处理循环引用问题。
js
let target = {
name: "peiqi",
address: {
x: 100,
y: 200,
},
};
target.address.z = target.address;
let result = JSON.stringify(target);
执行上面的代码会报错, 因为 JSON.stringify 无法处理存在处理循环引用问题
使用递归实现深拷贝
- 使用递归实现深拷贝就可以
解析特殊的值
及处理循环引用
!
代码实现
js
function deepClone(target) {
// 说明是对象或者是数组
if (target != null && typeof target === "object") {
let cloneTarget = target instanceof Array ? [] : {};
// 或者使用下面的方式
// let cloneTarget = Array.isArray(target) ? [] : {}
for (const key in target) {
if (target.hasOwnProperty(key)) {
// 此处使用递归
cloneTarget[key] = deepClone(target[key]);
}
}
return cloneTarget;
} else {
return target;
}
}
使用 Map 优化递归深拷贝
- 使用递归来实现深拷贝时,当在处理大型对象或深度嵌套结构时可能会导致性能问题。
- 为了优化递归深拷贝,可以使用
Map
数据结构可以在一定程度上优化深拷贝过程,特别是对于大型对象或深度嵌套结构。 Map
允许我们将已经拷贝过的对象作为键,将其对应的拷贝作为值存储起来,从而避免重复拷贝相同的对象。- 代码如下:
js
function deepCopyWithMap(obj, map = new Map()) {
if (map.has(obj)) {
return map.get(obj);
}
if (obj === null || typeof obj !== "object") {
return obj;
}
if (obj instanceof Array) {
const copyArr = [];
map.set(obj, copyArr); // 存入 Map,以便后续引用
for (let i = 0; i < obj.length; i++) {
copyArr[i] = deepCopyWithMap(obj[i], map);
}
return copyArr;
}
if (obj instanceof Object) {
const copyObj = {};
map.set(obj, copyObj); // 存入 Map,以便后续引用
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
copyObj[key] = deepCopyWithMap(obj[key], map);
}
}
return copyObj;
}
}