浅拷贝和深拷贝?
重要: 什么是拷贝?
首先来直观的感受一下什么是拷贝。
1 2 3 4 5
| let arr = [1, 2, 3]; let newArr = arr; newArr[0] = 100;
console.log(arr);
|
这是直接赋值的情况,不涉及任何拷贝。当改变newArr的时候,由于是同一个引用,arr指向的值也跟着改变。
现在进行浅拷贝:
1 2 3 4 5
| let arr = [1, 2, 3]; let newArr = arr.slice(); newArr[0] = 100;
console.log(arr);
|
当修改newArr的时候,arr的值并不改变。什么原因?因为这里newArr是arr浅拷贝后的结果,newArr和arr现在引用的已经不是同一块空间啦!
这就是浅拷贝!
但是这又会带来一个潜在的问题:
1 2 3 4 5
| let arr = [1, 2, {val: 4}]; let newArr = arr.slice(); newArr[2].val = 1000;
console.log(arr);
|
咦!不是已经不是同一块空间的引用了吗?为什么改变了newArr改变了第二个元素的val值,arr也跟着变了。
这就是浅拷贝的限制所在了。它只能拷贝一层对象。如果有对象的嵌套,那么浅拷贝将无能为力。但幸运的是,深拷贝就是为了解决这个问题而生的,它能解决无限极的对象嵌套问题,实现彻底的拷贝。当然,这是我们下一篇的重点。 现在先让大家有一个基本的概念。
接下来,我们来研究一下JS中实现浅拷贝到底有多少种方式?
浅拷贝
1. 手动实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| const shallowClone = source =>{ if(typeof source === 'object' && source !== null){ const output = Array.isArray(source) ? [] : {} ; for (let key in source){ if(source.hasOwnProperty(key)){ output[key] = source[key]; } } return output; }else{ return source; } }
|
2. Object.assign
Object.assign()
方法用于将所有可枚举属性的值从一个或多个源对象分配到目标对象。它将返回目标对象。
但是需要注意的是,Object.assgin() 拷贝的是对象的属性的引用,而不是对象本身。
1
| Object.assign(target, ...sources)
|
1 2 3
| let obj = { name: 'sy', age: 18 }; const obj2 = Object.assign({}, obj, {name: 'sss'}); console.log(obj2);
|
3. concat浅拷贝数组
concat()
方法用于合并两个或多个数组。此方法不会更改现有数组,而是返回一个新数组。
1 2 3 4
| let arr = [1, 2, 3]; let newArr = arr.concat(); newArr[1] = 100; console.log(arr);
|
4. slice浅拷贝
slice()
方法返回一个新的数组对象,这一对象是一个由 begin
和 end
决定的原数组的浅拷贝(包括 begin
,不包括end
)。原始数组不会被改变。
5. …展开运算符
1 2
| let arr = [1, 2, 3]; let newArr = [...arr];
|
6. Array.from
1 2 3 4 5 6 7
| let arr = [1, 2, { val: 4 }, () => { }]; arr.bar=arr; let newArr3 = Array.from(arr); newArr3[0] = 2000; newArr3[2].val = 2000; console.log(arr); console.log(newArr3);
|
深拷贝
第一种:JSON.parse(JSON.stringify(object))
估计这个api能覆盖大多数的应用场景,没错,谈到深拷贝,我第一个想到的也是它。但是实际上,对于某些严格的场景来说,这个方法是有巨大的坑的。问题如下:
- 无法解决
循环引用
的问题。举个例子:
1 2
| const a = {val:2}; a.target = a;
|
拷贝a会出现系统栈溢出,因为出现了无限递归
的情况。
无法拷贝一些特殊的对象
,诸如 RegExp, Date, Set, Map等。
无法拷贝函数
(划重点)。
会忽略 undefined
会忽略 symbol
就目前而言,第三方最完善的深拷贝方法是 Lodash 库的 _.cloneDeep()
方法了。在实际项目中,如需处理 JSON.stringify()
无法解决的 Case,我会推荐使用它。
面试够用版
主要运用到递归的思路去实现一个深拷贝方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| const deepClone = source =>{ const isArray = arr => Object.prototype.toString.call(arr) === '[object Array]'; const isObject = obj => obj !== null && (typeof obj === 'object' || typeof obj === 'function'); const copy = input =>{ if (typeof input === 'function' || !isObject(input)) return input;
const output = isArray(input) ? [] : {} ; for(let key in input){ if(input.hasOwnProperty(key)){ output[key] = copy(input[key]); } } return output; } return copy(source); }
let arr = [1, 2, {val: 4}]; let newArr1 = deepClone(arr); newArr1[2].val = 1000; let newArr2 = newArr1.slice(); newArr2[2].val = 2000;
console.log(arr); console.log(newArr1); console.log(newArr2)
|
需要进一步改进的问题
主要有循环引用、包装对象、函数、原型链、不可枚举属性、Map/WeakMap、Set/WeakSet、RegExp、Symbol、Date、ArrayBuffer、原生 DOM/BOM 对象等。
针对循环引用的问题
以下是一个循环引用(circular reference)的对象:
1 2
| const foo = { name: 'Frankie' } foo.bar = foo
|
使用WeakMap解决
首先,Map 的键属于强引用,而 WeakMap 的键则属于弱引用。且 WeakMap 的键必须是对象,WeakMap 的值则是任意的。
由于它们的键与值的引用关系,决定了 Map 不能确保其引用的对象不会被垃圾回收器回收的引用。假设我们使用的 Map,那么 foo
对象和我们深拷贝内部的 const map = new Map()
创建的 map
对象一直都是强引用关系,那么在程序结束之前,foo
不会被回收,其占用的内存空间一直不会被释放。
相比之下,原生的 WeakMap 持有的是每个键对象的“弱引用”,这意味着在没有其他引用存在时垃圾回收能正确进行。原生 WeakMap 的结构是特殊且有效的,其用于映射的 key 只有在其没有被回收时才是有效的。
基本上,如果你要往对象上添加数据,又不想干扰垃圾回收机制,就可以使用 WeakMap。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| const deepClone = source => { const weakmap = new WeakMap(); const isArray = arr => Object.prototype.toString.call(arr) === '[object Array]'; const isObject = obj => obj !== null && (typeof obj === 'object' || typeof obj === 'function'); const copy = input => { if (typeof input === 'function' || !isObject(input)) return input; if (weakmap.has(input)) { return weakmap.get(input) } const output = isArray(input) ? [] : {}; weakmap.set(input, output); for (let key in input) { if (input.hasOwnProperty(key)) { output[key] = copy(input[key]); } } return output; } return copy(source); }
|
这里提供另一个思路,也是可以的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| const deepCopy = source => {
const copiedArr = []
const copy = input => { if (typeof input === 'function' || !isObject(input)) return input; for (let i = 0, len = copiedArr.length; i < len; i++) { if (input === copiedArr[i].key) return copiedArr[i].value }
const output = isArray(input) ? [] : {}
copiedArr.push({ key: input, value: output })
}
return copy(source) }
|