1. 浅拷贝
浅拷贝是指,一个新的对象对原始对象的属性值进行精确地拷贝,如果拷贝的是基本数据类型,拷贝的就是基本数据类型的值,如果是引用数据类型,拷贝的就是内存地址。如果其中一个对象的引用内存地址发生改变,另一个对象也会发生变化。
浅拷贝的方法
(1)直接赋值
let arr1 = [1,2,3];let arr2 = arr1;new2[0] = 0;console.log(arr1); // [0, 2, 3]console.log(arr1); // [0, 2, 3]console.log(arr1 === arr2); // true
(2)Object.assign()
object.assign 是 ES6 中 object 的一个方法,该方法可以用于 JS 对象的合并等多个用途,其中一个用途就是可以进行浅拷贝。该方法接受的第一个参数是目标对象,其余参数是源对象,用法:Object.assign(target, source_1, ···),该方法可以实现浅拷贝,也可以实现一维对象的深拷贝。
注意:**
- 如果目标对象和源对象有同名属性,或者多个源对象有同名属性,则后面的属性会覆盖前面的属性。
- 如果该函数只有一个参数,当参数为对象时,直接返回该对象;当参数不是对象时,会先将参数转为对象然后返回。
- 因为
null和undefined不能转化为对象,所以第一个参数不能为null或undefined,会报错。 它不会拷贝对象的继承属性,不会拷贝对象的不可枚举的属性,可以拷贝 Symbol 类型的属性。
let target = {a: 1};let object2 = {b: 2};let object3 = {c: 3};Object.assign(target,object2,object3);console.log(target); // {a: 1, b: 2, c: 3}
(3)扩展运算符
使用扩展运算符可以在构造字面量对象的时候,进行属性的拷贝。语法:
let cloneObj = { ...obj };let obj1 = {a:1,b:{c:1}}let obj2 = {...obj1};obj1.a = 2;console.log(obj1); //{a:2,b:{c:1}}console.log(obj2); //{a:1,b:{c:1}}obj1.b.c = 2;console.log(obj1); //{a:2,b:{c:2}}console.log(obj2); //{a:1,b:{c:2}}
扩展运算符 和 object.assign 有同样的缺陷,也就是实现的浅拷贝的功能差不多,但是如果属性都是基本类型的值,使用扩展运算符进行浅拷贝会更加方便。
(4)数组方法实现数组浅拷贝
1)Array.prototype.slice
slice()方法是JavaScript数组的一个方法,这个方法可以从已有数组中返回选定的元素:用法:array.slice(start, end),该方法不会改变原始数组。该方法有两个参数,两个参数都可选,如果两个参数都不写,就可以实现一个数组的浅拷贝。let arr = [1,2,3,4];console.log(arr.slice()); // [1,2,3,4]console.log(arr.slice() === arr); //false
2)Array.prototype.concat
concat()方法用于合并两个或多个数组。此方法不会更改现有数组,而是返回一个新数组。该方法有两个参数,两个参数都可选,如果两个参数都不写,就可以实现一个数组的浅拷贝。let arr = [1,2,3,4];console.log(arr.concat()); // [1,2,3,4]console.log(arr.concat() === arr); //false
(5)手写实现浅拷贝
根据以上对浅拷贝的理解,实现一个浅拷贝的大致思路分为两点:
对基础类型做一个最基本的一个拷贝;
- 对引用类型开辟一个新的存储,并且拷贝一层对象属性。
// 浅拷贝的实现;function shallowCopy(object) {// 只拷贝对象if (!object || typeof object !== "object") return;// 根据 object 的类型判断是新建一个数组还是对象let newObject = Array.isArray(object) ? [] : {};// 遍历 object,并且判断是 object 的属性才拷贝for (let key in object) {if (object.hasOwnProperty(key)) {newObject[key] = object[key];}}return newObject;}
2. 深拷贝
深拷贝是指,对于简单数据类型直接拷贝他的值,对于引用数据类型,在堆内存中开辟一块内存用于存放复制的对象,并把原有的对象类型数据拷贝过来,这两个对象相互独立,属于两个不同的内存地址,修改其中一个,另一个不会发生改变。(1)Object.assign()
Object.assign() 方法可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。let obj1 = { person: {name: "kobe", age: 41},sports:'basketball' };let obj2 = Object.assign({}, obj1);obj2.person.name = "wade";obj2.sports = 'football'console.log(obj1); // { person: { name: 'wade', age: 41 }, sports: 'basketball' }
(2)JSON.stringify()
JSON.parse(JSON.stringify(obj))是目前比较常用的深拷贝方法之一,它的原理就是利用JSON.stringify将js对象序列化(JSON字符串),再使用JSON.parse来反序列化(还原)js对象。
这个方法可以简单粗暴的实现深拷贝,但是还存在问题,拷贝的对象中如果有函数,undefined,symbol,当使用过JSON.stringify()进行处理之后,都会消失。
let obj1 = { a: 0,b: {c: 0}};let obj2 = JSON.parse(JSON.stringify(obj1));obj1.a = 1;obj1.b.c = 1;console.log(obj1); // {a: 1, b: {c: 1}}console.log(obj2); // {a: 0, b: {c: 0}}
使用该方法时,需要注意以下几点:
- 无法拷贝不可枚举的属性;
- 无法拷贝对象的原型链;
- 拷贝 RegExp 引用类型会变成空对象;
- 对象中含有 NaN、Infinity 以及 -Infinity,JSON 序列化的结果会变成 null;
无法拷贝对象的循环应用,即对象成环 (obj[key] = obj)。
(3)函数库lodash的_.cloneDeep方法
该函数库也有提供_.cloneDeep用来做 Deep Copy
var _ = require('lodash');var obj1 = {a: 1,b: { f: { g: 1 } },c: [1, 2, 3]};var obj2 = _.cloneDeep(obj1);console.log(obj1.b.f === obj2.b.f);// false
(4)手写实现深拷贝函数
function clone(source) {//判断source是不是对象if (source instanceof Object == false) return source;//判断source是对象还是数组let target = Array.isArray(source) ? [] : {};for (let i in source) {if (source.hasOwnProperty(i)) {//判断数据i的类型if (typeof source[i] === 'object') {target[i] = clone(source[i]);} else {target[i] = source[i];}}}return target;}console.log(clone({b: {c: {d: 1}}})); // {b: {c: {d: 1}}})
虽然利用递归能实现一个深拷贝,但是同上面的 JSON.stringfy 一样,还是有一些问题没有完全解决,例如:
- 这个深拷贝函数并不能复制不可枚举的属性以及 Symbol 类型;
- 这种方法只是针对普通的引用类型的值做递归复制,而对于 Array、Date、RegExp、Error、Function 这样的引用类型并不能正确地拷贝;
-
3. 解决递归爆栈
我们使用递归的方法对数据进行拷贝,但是这也会出现一个问题,递归的深度的深度太深就会引发栈内存的溢出,我们使用下面的方法来解决递归爆栈的问题:将待拷贝的对象放入栈中,循环直至栈为空。
function cloneLoop(x) {const root = {};// 栈const loopList = [{parent: root,key: undefined,data: x,}];while(loopList.length) {// 深度优先const node = loopList.pop();const parent = node.parent;const key = node.key;const data = node.data;// 初始化赋值目标,key为undefined则拷贝到父元素,否则拷贝到子元素let res = parent;if (typeof key !== 'undefined') {res = parent[key] = {};}for(let k in data) {if (data.hasOwnProperty(k)) {if (typeof data[k] === 'object') {// 下一次循环loopList.push({parent: res,key: k,data: data[k],});} else {res[k] = data[k];}}}}return root;}
这样我们就解决了递归爆栈的问题,但是循环引用的问题依然存在。
4. 解决循环引用
举例:当a对象的中的某属性值为a对象,这样就会造成循环引用。
我们使用暴力破解的方法来解决循环引用的问题。
思路:引入一个数组uniqueList用来存储已经拷贝的数组,每次循环遍历时,先判断对象是否在uniqueList中了,如果在的话就不执行拷贝逻辑了function cloneForce(x) {const uniqueList = []; // 用来去重let root = {};const loopList = [{parent: root,key: undefined,data: x,}];while(loopList.length) {const node = loopList.pop();const parent = node.parent;const key = node.key;const data = node.data;// 初始化赋值目标,key为undefined则拷贝到父元素,否则拷贝到子元素let res = parent;if (typeof key !== 'undefined') {res = parent[key] = {};}// 数据已经存在let uniqueData = find(uniqueList, data);if (uniqueData) {parent[key] = uniqueData.target;continue;}// 数据不存在// 保存源数据,在拷贝数据中对应的引用uniqueList.push({source: data,target: res,});for(let k in data) {if (data.hasOwnProperty(k)) {if (typeof data[k] === 'object') {loopList.push({parent: res,key: k,data: data[k],});} else {res[k] = data[k];}}}}return root;}//find函数用来遍历uniqueListfunction find(arr, item) {for(let i = 0; i < arr.length; i++) {if (arr[i].source === item) {return arr[i];}}return null;}
5. 总结
浅拷贝:浅拷贝指的是将一个对象的属性值复制到另一个对象,如果有的属性的值为引用类型的话,那么会将这个引用的地址复制给对象,因此两个对象会有同一个引用类型的引用。浅拷贝可以使用 Object.assign 和展开运算符来实现。
- 深拷贝:深拷贝相对浅拷贝而言,如果遇到属性值为引用类型的时候,它新建一个引用类型并将对应的值复制给它,因此对象获得的一个新的引用类型而不是一个原有类型的引用。深拷贝对于一些对象可以使用 JSON 的两个函数来实现,但是由于 JSON 的对象格式比 js 的对象格式更加严格,所以如果属性值里边出现函数或者 Symbol 类型的值时,会转换失败 ```javascript // 浅拷贝的实现;
function shallowCopy(object) { // 只拷贝对象 if (!object || typeof object !== “object”) return;
// 根据 object 的类型判断是新建一个数组还是对象 let newObject = Array.isArray(object) ? [] : {};
// 遍历 object,并且判断是 object 的属性才拷贝 for (let key in object) { if (object.hasOwnProperty(key)) { newObject[key] = object[key]; } }
return newObject; }
// 深拷贝的实现;
function deepCopy(object) { if (!object || typeof object !== “object”) return;
let newObject = Array.isArray(object) ? [] : {};
for (let key in object) { if (object.hasOwnProperty(key)) { newObject[key] = typeof object[key] === “object” ? deepCopy(object[key]) : object[key]; } }
return newObject; } ```
