JS 中对象的深浅拷贝
拷贝我们都知道这个词的意思,我们经常做过复制、粘贴的操作,其中的复制就是拷贝,那么在拷贝的时候,如果我们复制出来的内容和原内容是完全的分开,各自不相影响,那么这就属于深拷贝,如果不是完完全全的分开的,在某些情况下修改其一,另一方也会跟着改变,那么这就是浅拷贝。
在 JS 中,我们知道有两种类型值,① 原始值 ② 引用值,对于这两种值的复制情况是不一样的。不清楚的可以参考JS 中原始值和引用值的区别。
一、浅拷贝
1.1 原始值和引用值的拷贝
先来看看对原始值和引用值的拷贝例子:
let a = 1;
let b = a;
console.log(a,b); // 1 1
b = 3; // 改变 b ,a 不会改变
console.log(a,b); // 1 3
a = 2; // 改变 a ,b 不会改变
console.log(a,b); // 2 3
let a = {
name:"js"
}
let b = a;
console.log(a,b); // {name: "js"} {name: "js"}
b.name = "html"; // 改变 b 所指向的内容 ,a 所指的内容也会变
console.log(a,b); // {name: "html"} {name: "html"}
a.name = "css"; // 改变 a 所指向的内容 ,b 所指的内容也会变
console.log(a,b); // {name: "css"} {name: "css"}
由上述两段代码很清楚的看出,原始值与引用值的拷贝都是复制变量当中的值,只是原始值的变量内存储的内容就是原始值,而引用值变量内存储的是引用地址。
1.2 ES3 中对象的浅拷贝
在 JS 中数组也是对象,对象包括数组在内。由于在字面量形式上有所区别(例如:中括号[] 表示数组,大括号{} 表示对象),所以在拷贝的时候需要加以区分,先来看看区分数组和对象的常用方式如下,当然还有其它方法也可以判断,在本文中只需要用到下面介绍的方式。
// 1.通过 typeof 关键字【无法识别】数组,与对象
console.log(typeof []); // object
console.log(typeof {}); // object
// 2.通过 instanceof 关键字来【识别数组】
console.log([] instanceof Array); // true
console.log({} instanceof Array); // false
console.log([] instanceof Object); // true
console.log({} instanceof Object); // true
// 3.通过 ES6 提供的数组方法 isArray() 来【识别数组】
console.log(Array.isArray([])); // true
console.log(Array.isArray({})); // false
- ES3 中 for-in 对对象的浅复制算法:
function simpleClone(obj){
// 判断参数是否是数组,如果是则声明一个空数组保存复制结果,
// 否则声明一个空对象保存复制结果
let clone = obj instanceof Array ? [] : {};
for (const key in obj) { // for in 循环的是对象的键,数组的话就是索引
clone[key] = obj[key];
}
return clone;
}
对对象的浅复制示例:
let obj = {
a:1,
b:"第一层b",
c:{
c1:2,
c2:"第二层c2"
},
d:[1,2,3]
};
let cObj = simpleClone(obj);
console.log(obj); // {a: 1, b: "第一层b", c: {c1: 2, c2: "第二层c2"}, d: [1, 2, 3]}
console.log(cObj); // {a: 1, b: "第一层b", c: {c1: 2, c2: "第二层c2"}, d: [1, 2, 3]}
obj.a = 10; // 修改原数据中的原始值类型,不会影响到复制的数据
obj.c.c1 = 10; // 修改原数据中的引用值类型,会影响到复制的数据
console.log(obj); // {a: 10, b: "第一层b", c: {c1: 10, c2: "第二层c2"}, d: [1, 2, 3]}
console.log(cObj); // {a: 1, b: "第一层b", c: {c1: 10, c2: "第二层c2"}, d: [1, 2, 3]}
对数组的浅复制示例:
let arr = [1,"js",[2,'css'],{a:3,b:4}];
console.log(arr); // [1, "js", [2, "css"], {a: 3, b: 4}]
console.log(cArr); // [1, "js", [2, "css"], {a: 3, b: 4}]
cArr[0] = 10; // 修改复制数据中的原始值类型,不会影响到原始数据
cArr[2][0]= 10; // 修改复制数据中的数组内数据,会影响到原始数据
cArr[3].a= 10; // 修改复制数据中的对象内数据,会影响到原始数据
console.log(arr); // [1, "js", [10, "css"], {a: 10, b: 4}]
console.log(cArr); // [10, "js", [10, "css"], {a: 10, b: 4}]
1.3 ES5 中对象的浅拷贝
在 ES5 中有 Object.getOwnPropertyNames() 方法可以获取到对象【属性的数组】。如果对象是数组则获取的是 下标 和 length 属性构成的数组。如下:
let obj = { // 对象数据
a:1,
b:"第一层b",
c:{
c1:2,
c2:"第二层c2"
},
d:[1,2,3]
}
let arr = [1,"js",[2,'css'],{a:3,b:4}] // 数组数据
console.log(Object.getOwnPropertyNames(obj)); // ["a", "b", "c", "d"]
console.log(Object.getOwnPropertyNames(arr)); // ["0", "1", "2", "3", "length"]
- ES5 中我们就可以用 for each 语法结合 Object.getOwnPropertyNames() 方法来实现对象的浅拷贝:
function simpleClone(obj){
let clone = obj instanceof Array ? [] : {};
let arr = Object.getOwnPropertyNames(obj);
arr.forEach(function(key){
clone[key] = obj[key];
});
return clone;
}
- 还可以更底层的通过属性描述符的方式来实现拷贝。属性描述的知识
function simpleClone(obj){
let clone = obj instanceof Array ? [] : {};
let arr = Object.getOwnPropertyNames(obj);
arr.forEach(function(key){
// des 保存 obj 对象上 key 属性的描述
let des = Object.getOwnPropertyDescriptor(obj,key);
// 把 clone 对象上的 key 属性(没有则创建一个)设置为 des 这种描述
Object.defineProperty(clone,key,des);
});
return clone;
}
1.4 ES6中对象的浅拷贝
在 ES6 中,我们可以用 Object.keys()、Object.values() 和 Object.entries() 函数分别获得可迭代对象的【键的数组】、【值的数组】和【键值对的数组】。如下:
let obj = { // 对象数据
a:1,
b:"第一层b",
c:{
c1:2,
c2:"第二层c2"
},
d:[1,2,3]
};
console.log(Object.keys(obj)); // ["a", "b", "c", "d"]
console.log(Object.values(obj)); // [1, "第一层b", {c1: 2, c2: "第二层c2"}, [1, 2, 3]]
console.log(Object.entries(obj)); // [["a", 1], ["b", "第一层b"], ["c", {c1: 2, c2: "第二层c2"}], ["d", [1, 2, 3]]]
let arr = [1,"js",[2,'css'],{a:3,b:4}]; // 数组数据 keys() 获取的就是索引
console.log(Object.keys(arr)); // ["0", "1", "2", "3"]
console.log(Object.values(arr)); // [1, "js", [2, "css"], {a: 3, b: 4}]
console.log(Object.entries(arr)); // [["0", 1], ["1", "js"], ["2", [2, "css"]], ["3", {a: 3, b: 4}]]
- 有了这三个函数就很容易用 for of 循环来浅拷贝对象了:
function simpleClone(obj){
let clone = obj instanceof Array ? [] : {};
for (const key of Object.keys(obj)) {
clone[key] = obj[key];
}
return clone;
}
function simpleClone(obj){
let clone = obj instanceof Array ? [] : {};
for (const [key,value]of Object.entries(obj)) {
clone[key] = value;
}
return clone;
}
二、深拷贝
通过前面介绍的浅拷贝方式,应该比较容易发现拷贝没有将原数据和拷贝后的数据完全隔绝开的原因在于原数据内部保存有引用类型的数据。因此,要实现深拷贝,就需要在碰到引用类型的内部数据时,需要进一步的去拷贝引用类型数据所指向的更深一层的数据。因此用递归方法就能很好的解决多层的拷贝问题。下面几段代码分别是用递归对上述中浅拷贝的算法进行改进。
// ES3 中的 for in 方式 + 递归实现深拷贝
function deepClone(obj){
if(obj === null) return null;
let clone = obj instanceof Array ? [] : {};
for (let key in obj) {
// 主要判断【obj[key]】还是不是对象,如果是,递归,不是则赋值
clone[key] = typeof obj[key] === "object" ? deepClone(obj[key]) : obj[key];
}
return clone;
}
// ES5 中的 for each 方式 + 递归实现深拷贝
function deepClone(obj){
if(obj === null) return null;
let clone = obj instanceof Array ? [] : {};
let arr = Object.getOwnPropertyNames(obj);
arr.forEach(key => clone[key] = typeof obj[key] === "object" ? deepClone(obj[key]) : obj[key]);
return clone;
}
// ES6 中的 for of 方式 + keys() + 递归实现深拷贝
function deepClone(obj){
if (obj === null) return null;
let clone = obj instanceof Array ? [] : {};
for (const key of Object.keys(obj)) {
clone[key] = typeof obj[key] === "object" ? deepClone(obj[key]) : obj[key];
}
return clone;
}
// ES6 中的 for of 方式 + entries() + 递归实现深拷贝
function deepClone(obj){
if (obj === null) return null;
let clone = obj instanceof Array ? [] : {};
for (const [key,value] of Object.entries(obj)) {
clone[key] = typeof value === "object" ? deepClone(value) : value;
}
return clone;
}
// 深拷贝数据验证
let obj = {
a:1,
b:"第一层b",
c:{
c1:2,
c2:"第二层c2"
},
d:[1,2,3]
}
let cObj = deepClone(obj);
cObj.a = 10;
cObj.c.c1 = 10;
console.log(`修改后原数据:`);
console.log(obj);
console.log(`修改后拷贝的数据:`);
console.log(cObj);