什么是拷贝?
let arr = [1, 2, 3];let newArr = arr;newArr[0] = 100;
console.log(arr);//[100, 2, 3]这是直接赋值的情况,不涉及任何拷贝。当改变newArr的时候,由于是同一个引用,arr指向的值也跟着改变。
浅拷贝
上面的那个例子进行浅拷贝
let arr = [1, 2, 3];let newArr = arr.slice();newArr[0] = 100;
console.log(arr);//[1, 2, 3]当修改newArr的时候,arr的值并不改变。什么原因?因为这里newArr是arr浅拷贝后的结果,newArr和arr现在引用的已经不是同一块空间!!!
这就是浅拷贝!
但是这又会带来一个潜在的问题:
let arr = [1, 2, {val: 4}];let newArr = arr.slice();newArr[2].val = 1000;
console.log(arr);//[ 1, 2, { val: 1000 } ]咦!不是已经不是同一块空间的引用了吗?为什么改变了newArr改变了第二个元素的val值,arr也跟着变了。
这就是浅拷贝的限制所在了。它只能拷贝一层对象。如果有对象的嵌套,那么浅拷贝将无能为力。但幸运的是,深拷贝就是为了解决这个问题而生的,它能 解决无限极的对象嵌套问题,实现彻底的拷贝。当然,这是我们下一篇的重点。 现在先让大家有一个基本的概念。
接下来,我们来研究一下JS中实现浅拷贝到底有多少种方式?
浅拷贝API
1. Object.assign
但是需要注意的是,Object.assgin() 拷贝的是对象的属性的引用,而不是对象本身。
let obj = { name: 'sy', age: 18 };const obj2 = Object.assign({}, obj, {name: 'sss'});console.log(obj2);//{ name: 'sss', age: 18 }2. concat浅拷贝数组
let arr = [1, 2, 3];let newArr = arr.concat();newArr[1] = 100;console.log(arr);//[ 1, 2, 3 ]3. slice
let arr = [1, 2, 3];let newArr = arr.slice();newArr[1] = 100;console.log(arr);//[ 1, 2, 3 ]4. …展开运算符
let arr = [1, 2, 3];let newArr = [...arr];//跟arr.slice()是一样的效果手动实现浅拷贝
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一套组合拳
JSON.parse(JSON.stringify());估计这个api能覆盖大多数的应用场景,没错,谈到深拷贝,我第一个想到的也是它。但是实际上,对于某些严格的场景来说,这个方法是有巨大的坑的。问题如下:
- 无法解决
循环引用的问题。举个例子:
const a = {val:2};a.target = a;拷贝a会出现系统栈溢出,因为出现了无限递归的情况。
- 无法拷贝一写
特殊的对象,诸如 RegExp, Date, Set, Map等 - 无法拷贝函数(划重点)
2. 简单实现
只需将上面手写的浅拷贝骚做修改
function deepClone(target) { if (typeof target === "object" && target !== null) { let temp = Array.isArray(target) ? [] : {}; for (const key in target) { if (Object.hasOwnProperty.call(target, key)) { const element = target[key]; temp[key] = deepClone(element); } } return temp; } return target;}3. 细节以及问题处理最终实现
如上所述,JSON.parse(JSON.stringfy(obj))有三个问题,我们根据这三个问题一个个解决
1.循环引用问题解决
假如是这样的情况下,a对象中的一个属性有引用了自己
const a = { name: "weng", age: 12, };
a.target = a;这里我们可以使用一个Map,记录下已经拷贝过了的对象,如果已经拷贝过了,就直接返回这个对象
function deepClone(target, map = new Map()) { if (map.get(target)) { return target; } if ( (typeof target === "object" || typeof target === "function") && target !== null ) { map.set(target, true); let temp = Array.isArray(target) ? [] : {}; for (const key in target) { if (Object.hasOwnProperty.call(target, key)) { const element = target[key]; temp[key] = deepClone(element, map); } } return temp; } return target;}好了咱们试一下
const a = { name: "weng", age: 12,};
a.target = a;console.log(deepClone(a));
好像是没有问题了, 拷贝也完成了。但还是有一个潜在的坑, 就是map 上的 key 和 map 构成了强引用关系,这是相当危险的。我给你解释一下与之相对的弱引用的概念你就明白了:
在计算机程序设计中,弱引用与强引用相对,
是指不能确保其引用的对象不会被垃圾回收器回收的引用。 一个对象若只被弱引用所引用,则被认为是不可访问(或弱可访问)的,并因此可能在任何时刻被回收。 —百度百科
说的有一点绕,我用大白话解释一下,被弱引用的对象可以在任何时候被回收,而对于强引用来说,只要这个强引用还在,那么对象无法被回收。拿上面的例子说,map 和 a一直是强引用的关系, 在程序结束之前,a 所占的内存空间一直不会被释放。
怎么解决这个问题?
很简单,让 map 的 key 和 map 构成弱引用即可。ES6给我们提供了这样的数据结构,它的名字叫WeakMap,它是一种特殊的Map, 其中的键是弱引用的。其键必须是对象,而值可以是任意的。
稍微改造一下即可:
function deepClone(target, map = new Map()) { .....}2. 特殊对象的拷贝
对于特殊的对象,我们使用以下方式来鉴别:
Object.prototype.toString.call(obj);梳理一下对于可遍历对象会有什么结果:
"object Map""object WeakMap""object Set""object WeakSet""object Array""object Object""object Arguments"以及不可便利的对象
"object Boolean""object Symbol""object Number""object String""object Date""object Error""object RegExp""object Function"对于不同的对象有不同的处理方案,但是很多是相互类似的
之后改进之后的代码:
function getType(target) { return Object.prototype.toString.call(target); }
function isObject(target) { return typeof target === "object" && target !== null; }
function deepClone(target, map = new WeakMap()) { if (!isObject(target)) { return target; }
if (map.get(target)) { return target; }
// 获取target是Object的哪种衍生类型 const targetType = getType(target);
let cloneTarget;
map.set(target, true);
const structFn = Object.getPrototypeOf(target).constructor;
switch (targetType) { case "[object RegExp]": const { source, flags } = target; cloneTarget = new structFn(source, flags); break; case "[object Function]": // 函数这块独立出来处理 break; case "[object Map]": case "[object WeakMap]": cloneTarget = new structFn(); target.forEach((value, key) => { cloneTarget.set(deepClone(key), deepClone(value)); }); break; case "[object Set]": case "[object WeakSet]": cloneTarget = new structFn(); target.forEach((value) => { cloneTarget.add(deepClone(value)); }); break; case "[object Array]": case "[object Object]": cloneTarget = new structFn(); for (const key in target) { if (Object.hasOwnProperty.call(target, key)) { const element = target[key]; cloneTarget[key] = deepClone(element, map); } } break; default: // case "[object String]": // case "[object Number]": // case "[object Boolean]": // case "[object Error]": // case "[object Date]": cloneTarget = new structFn(Object.prototype.valueOf.call(target)); break; }
return cloneTarget; }其中这行代码 const structFn = Object.getPrototypeOf(target).constructor;是为了获取衍生对象或者对象的构造函数,是为了防止丢失原型的情况,之后利用构造函数去整活。
3. 处理函数类型
虽然函数也是对象,但是它过于特殊,我们单独把它拿出来拆解。
提到函数,在JS种有两种函数,一种是普通函数,另一种是箭头函数。每个普通函数都是 Function的实例,而箭头函数不是任何类的实例,每次调用都是不一样的引用。那我们只需要 处理普通函数的情况,箭头函数直接返回它本身就好了。
那么如何来区分两者呢?
答案是: 利用原型。箭头函数是不存在原型的。
处理代码如下:
const handleFunc = (func) => { // 箭头函数直接返回自身 if(!func.prototype) return func; const bodyReg = /(?<={)(.|\n)+(?=})/m; const paramReg = /(?<=\().+(?=\)\s+{)/; const funcString = func.toString(); // 分别匹配 函数参数 和 函数体 const param = paramReg.exec(funcString); const body = bodyReg.exec(funcString); if(!body) return null; if (param) { const paramArr = param[0].split(','); return new Function(...paramArr, body[0]); } else { return new Function(body[0]); }}4. 优化代码
在上面的代码中,我们在处理一些”基本类型“对象的时候,我们去拿他们的构造函数,然后给他new出来,但是ES6中不推荐这样直接new String, 或者 new Number的写法,并且Symbol类型是无法被new操作符调用的,所以我们改成 new Object的形式并且传他们的value值
cloneTarget = new Object(Object.prototype.valueOf.call(target));最终代码
function getType(target) { return Object.prototype.toString.call(target); }
function isObject(target) { return typeof target === "object" && target !== null; }
function deepClone(target, map = new WeakMap()) { if (!isObject(target)) { return target; }
if (map.get(target)) { return target; }
// 获取target是Object的哪种衍生类型 const targetType = getType(target);
let cloneTarget;
map.set(target, true);
const structFn = Object.getPrototypeOf(target).constructor;
switch (targetType) { case "[object RegExp]": const { source, flags } = target; cloneTarget = new structFn(source, flags); break; case "[object Function]": // 函数这块独立出来处理 cloneTarget = handleFunc(target); break; case "[object Map]": case "[object WeakMap]": cloneTarget = new structFn(); target.forEach((value, key) => { cloneTarget.set(deepClone(key), deepClone(value)); }); break; case "[object Set]": case "[object WeakSet]": cloneTarget = new structFn(); target.forEach((value) => { cloneTarget.add(deepClone(value)); }); break; case "[object Array]": case "[object Object]": cloneTarget = new structFn(); for (const key in target) { if (Object.hasOwnProperty.call(target, key)) { const element = target[key]; cloneTarget[key] = deepClone(element, map); } } break; default: cloneTarget = new Object(Object.prototype.valueOf.call(target)); break; }
return cloneTarget; }
const handleFunc = (func) => { // 箭头函数直接返回自身 if(!func.prototype) return func; const bodyReg = /(?<={)(.|\n)+(?=})/m; const paramReg = /(?<=\().+(?=\)\s+{)/; const funcString = func.toString(); // 分别匹配 函数参数 和 函数体 const param = paramReg.exec(funcString); const body = bodyReg.exec(funcString); if(!body) return null; if (param) { const paramArr = param[0].split(','); return new Function(...paramArr, body[0]); } else { return new Function(body[0]); }}