浅拷贝
自己创建一个新的对象,来接受你要重新复制或引用的对象值。如果对象属性是基本的数据类型,复制的就是基本类型的值给新对象;但如果属性是引用数据类型,复制的就是内存中的地址,如果其中一个对象改变了这个内存中的地址,肯定会影响到另一个对象。
1. object.assign
object.assign 是 ES6 中 object 的一个方法,该方法可以用于JS 对象的合并等多个用途,其中一个用途就是可以进行浅拷贝。
该方法的第一个参数是拷贝的目标对象,后面的参数是拷贝的来源对象(也可以是多个来源)。
object.assign 的语法为:Object.assign(target, ...sources)
const target = {}; const source = { a: { b: 1 } }; Object.assign(target, source); console.log(target); // { a: { b: 1 } };
但是使用 object.assign 方法有几点需要注意:
- 它不会拷贝对象的继承属性;
- 它不会拷贝对象的不可枚举的属性;
- 可以拷贝 Symbol 类型的属性。
const obj1 = { a: { b: 1 }, c: 1, sym: Symbol(1) }; Object.defineProperty(obj1, 'innumerable', { value: '1', enumerable: false }); const obj2 = {}; Object.assign(obj2, obj1) obj1.a.b = 2; obj1.c = 2; console.log('obj1', obj1); // {a: {b: 2}, c: 2, sym: Symbol(1), innumerable: "1"} console.log('obj2', obj2); // {a: {b: 2}, c: 1, sym: Symbol(1)}
从上面的样例代码中可以看到,利用 object.assign 也可以拷贝 Symbol 类型的对象,但是如果到了对象的第二层属性 obj1.a.b 这里的时候,前者值的改变也会影响后者的第二层属性的值,说明其中依旧存在着访问共同堆内存的问题,也就是说这种方法还不能进一步复制,而只是完成了浅拷贝的功能。
2. 扩展运算符方式
扩展运算符的语法为:let cloneObj = { ...obj };
/* 对象的拷贝 */ const obj = { a: 1, b: { c: 1 } } const obj2 = { ...obj } obj.a = 2 console.log(obj) //{a:2,b:{c:1}} console.log(obj2); //{a:1,b:{c:1}} obj.b.c = 2 console.log(obj) //{a:2,b:{c:2}} console.log(obj2); //{a:1,b:{c:2}} /* 数组的拷贝 */ let arr = [1, 2, 3]; let newArr = [...arr]; //跟arr.slice()是一样的效果
扩展运算符 和 object.assign 有同样的缺陷,也就是实现的浅拷贝的功能差不多,但是如果属性都是基本类型的值,使用扩展运算符进行浅拷贝会更加方便。
3. concat 拷贝数组
数组的 concat 方法其实也是浅拷贝,所以连接一个含有引用类型的数组时,需要注意修改原数组中的元素的属性,因为它会影响拷贝之后连接的数组。不过 concat 只能用于数组的浅拷贝,使用场景比较局限。
const arr = [1, 2, 3, { a: 1 }]; const newArr = arr.concat(); newArr[1] = 0; console.log(arr); // [ 1, 2, 3, {a: 1} ] console.log(newArr); // [ 1, 0, 3, { a: 1 } ] newArr[3].a = 4; console.log(arr); // [ 1, 2, 3, {a: 4} ] console.log(newArr); // [ 1, 2, 3, {a: 4} ]
4. slice 拷贝数组
slice 方法也比较有局限性,因为它仅仅针对数组类型。slice 方法会返回一个新的数组对象,这一对象由该方法的前两个参数来决定原数组截取的开始和结束位置,是不会影响和改变原始数组的。但是,数组元素是引用类型的话,也会影响到原始数组。
slice 的语法为:arr.slice(begin, end);
const arr = [1, 2, { val: 4 }]; const newArr = arr.slice(); newArr[2].val = 5; newArr[1] = 3; console.log(arr); //[ 1, 2, { val: 5 } ]
从上面我们可以看出,这就是浅拷贝的限制所在——它只能拷贝一层对象。如果存在对象的嵌套,那么浅拷贝将无能为力。因此深拷贝就是为了解决这个问题而生的,它能解决多层对象嵌套问题,彻底实现拷贝。
我们总结一下浅拷贝的原理:
- 对基础类型做一个最基本的一个拷贝;
- 对引用类型开辟一个新的存储,并且拷贝一层对象属性。
以下是对浅拷贝的简单实现:
const shallowClone = (target) => { if (typeof target === 'object' && target !== null) { const cloneTarget = Array.isArray(target) ? []: {}; for (let prop in target) { if (target.hasOwnProperty(prop)) { cloneTarget[prop] = target[prop]; } } return cloneTarget; } else { return target; } }
深拷贝
浅拷贝只是创建了一个新的对象,复制了原有对象的基本类型的值,而引用数据类型只拷贝了一层属性,再深层的还是无法进行拷贝。深拷贝则不同,对于复杂引用数据类型,其在堆内存中完全开辟了一块内存地址,并将原有的对象完全复制过来存放。
深拷贝后的对象与原始对象是相互独立、不受影响的,彻底实现了内存上的分离。
总的来说,深拷贝的原理可以总结如下:
将原对象从内存中完整地拷贝出来一份给新对象,并从堆内存中开辟一个全新的空间存放新对象,且新对象的修改并不会改变原对象,二者实现真正的分离。
1. JSON.stringify
把一个对象序列化成为 JSON 的字符串,并将对象里面的内容转换成字符串,最后再用 JSON.parse() 的方法将JSON 字符串生成一个新的对象。
const obj1 = { a: 1, b: [1, 2, 3] } const str = JSON.stringify(obj1); const obj2 = JSON.parse(str); console.log(obj2); //{a:1,b:[1,2,3]} obj1.a = 2; obj1.b.push(4); console.log(obj1); //{a:2,b:[1,2,3,4]} console.log(obj2); //{a:1,b:[1,2,3]}
可以看到通过改变 obj1 的 b 属性,其实可以看出 obj2 这个对象也不受影响。
但是,JSON.stringify并不是那么完美的,它也有局限性。
- 拷贝的对象的值中如果有函数、undefined、symbol 这几种类型,经过 JSON.stringify 序列化之后的字符串中这个键值对会消失;
- 拷贝 Date 引用类型会变成字符串;
- 无法拷贝不可枚举的属性;
- 无法拷贝对象的原型链;
- 拷贝 RegExp 引用类型会变成空对象;
- 对象中含有 NaN、Infinity 以及 -Infinity,JSON 序列化的结果会变成 null;
- 无法拷贝对象的循环应用,即对象成环 (obj[key] = obj)。
我们通过代码来看下效果。
let obj = { func: function () { alert(1) }, obj: { a: 1 }, arr: [1, 2, 3], und: undefined, reg: /123/, date: new Date(0), NaN: NaN, infinity: Infinity, sym: Symbol('1') } Object.defineProperty(obj, 'innumerable', { enumerable: false, value: 'innumerable' }); console.log('obj', obj); // { NaN: NaN , arr: (3) [1, 2, 3] ,date: Thu Jan 01 1970 08:00:00 GMT+0800 (中国标准时间) {}, func: ƒ(), infinity: Infinity , obj: { a: 1 } , reg: /123/, sym: Symbol(1), und: undefined, innumerable: "innumerable" } const str = JSON.stringify(obj); const obj1 = JSON.parse(str); console.log('obj1', obj1); // { NaN: null, arr: (3) [1, 2, 3], date: "1970-01-01T00:00:00.000Z", infinity: null, obj: {a: 1}, reg: {} }
2. 实现深拷贝方法
下面是一个实现 deepClone 函数封装的例子,有几点需要注意下。
- WeakMap 是弱引用类型,可以有效防止内存泄漏;
- 能够遍历对象的不可枚举属性以及 Symbol 类型,我们可以使用 Reflect.ownKeys 方法;
- 利用 Object 的 getOwnPropertyDescriptors 方法可以获得对象的所有属性,以及对应的特性;
- 结合 Object 的 create 方法创建一个新对象,并继承传入原对象的原型链;
- 当参数为 Date、RegExp 类型,则直接生成一个新的实例返回;
const isComplexDataType = obj => (typeof obj === 'object' || typeof obj === 'function') && (obj !== null) const deepClone = function (obj, hash = new WeakMap()) { if (obj.constructor === Date) return new Date(obj) // 日期对象直接返回一个新的日期对象 if (obj.constructor === RegExp) return new RegExp(obj) //正则对象直接返回一个新的正则对象 //如果循环引用了就用 weakMap 来解决 if (hash.has(obj)) return hash.get(obj) let allDesc = Object.getOwnPropertyDescriptors(obj) //遍历传入参数所有键的特性 let cloneObj = Object.create(Object.getPrototypeOf(obj), allDesc) //继承原型链 hash.set(obj, cloneObj) for (let key of Reflect.ownKeys(obj)) { cloneObj[key] = (isComplexDataType(obj[key]) && typeof obj[key] !== 'function') ? deepClone(obj[key], hash) : obj[key] } return cloneObj } // 下面是验证代码 let obj = { num: 0, str: '', boolean: true, unf: undefined, nul: null, obj: { name: '我是一个对象', id: 1 }, arr: [0, 1, 2], func: function () { console.log('我是一个函数') }, date: new Date(0), reg: new RegExp('/我是一个正则/ig'), [Symbol('1')]: 1, }; Object.defineProperty(obj, 'innumerable', { enumerable: false, value: '不可枚举属性' } ); obj = Object.create(obj, Object.getOwnPropertyDescriptors(obj)) obj.loop = obj // 设置loop成循环引用的属性 let cloneObj = deepClone(obj) cloneObj.arr.push(4) console.log('obj', obj) console.log('cloneObj', cloneObj)