JS基础 - 手写深拷贝
参考:https://juejin.im/post/6844903929705136141#heading-9
提要
- 基本实现:递归,数组与对象
- 处理循环引用:使用map记录已克隆过的对象
- 克隆更多的类型:可遍历类型(Array/Set/Map/Object); 不可遍历类型(String/Number/Symbol/RegExp)
=>如何获取引用类型的具体类型;通用的引用类型初始化函数;为什么要将Symbol转换为Object;正则表达式的构造函数 - 克隆函数:普通函数;箭头函数
=> 如何区分普通函数和箭头函数;函数体和参数的正则表达式;参数为空的考虑
基础版
思路:深拷贝一个对象(数组),就是要深拷贝对象(数组)中的每一个元素,很显然应当用递归实现。
- 递归退出条件:当传入的变量是基本类型,或为null时,直接返回变量本身。
- 递推工作:深拷贝每一个属性,注意用hasOwnProperty保证只拷贝自身的属性,不拷贝继承到的属性。
代码:
function deepClone(obj){ //不是引用类型或为null时退出递归 if(typeof obj !== "object" || obj == null){ return obj; } //初始化新变量 let copy; if(obj instanceof Array){ copy = []; }else{ copy = {}; } //深拷贝每一个元素 for(let key in obj){ if(obj.hasOwnProperty(key)){ copy[key] = deepClone(obj[key]); } } return copy; }
循环引用
如果一个对象的属性引用了自身,上述代码会使程序无限递归。我们可以使用一个map,记录已经拷贝过的所有对象,在拷贝时,首先检查map中是否存在该对象,若有,则直接返回,否则,将当前对象记录到map中,key为其本身,value为其克隆体,然后继续正常的克隆。
function deepClone(obj, map = new Map()){ //不是引用类型或为null时退出递归 if(typeof obj !== "object" || obj == null){ return obj; } //检查是否已经克隆过 if(map.get(obj)){ return map.get(obj); } //初始化新变量 let copy; if(obj instanceof Array){ copy = []; }else{ copy = {}; } //加入map map.set(obj, copy); //深拷贝每一个元素 for(let key in obj){ if(obj.hasOwnProperty(key)){ copy[key] = deepClone(obj[key], map); } } return copy; }
考虑更多的数据类型
首先,使用toString来获取引用类型具体的类型:
每一个引用类型都有toString()方法,如果该方法没有被重写过,则返回“[object type]”,其中type是对象的具体类型。
由于大部分引用类型都重写了toString(),我们需要直接调用Object原型上的toString()
function getType(obj) { return Object.prototype.toString.call(obj); }
我们可以大致将各种引用类型分为两类:需要递归拷贝的,如Array/Set/Map;不需要递归拷贝的,如Number/Boolean/String
首先看需要递归拷贝的,我们需要获取他们的初始化数据,即获取其构造函数,然后创建一个空对象并返回:
function getInit(obj) { const constructor = target.constructor; return new constructor(); }
可以克隆set和map的完整方法:
const mapTag = getType(new Map()); const setTag = getType(new Set()); const arrayTag = getType(new Array()); const objectTag = getType(new Object()); //判断是否需要递归拷贝,即是非null的引用类型 function isObject(variable){ return (variable != null) && (typeof variable == "object" || typeof variable == "function"); } //通过toString获取具体变量类型 function getType(obj){ return Object.prototype.toString.call(obj); } //获取构造函数并返回空对象 function getInit(obj){ const Constructor = obj.constructor; return new Constructor(); } function deepClone(obj, map = new Map()){ //不需要递归拷贝,直接克隆值 if(!isObject(obj)){ return obj; } //防止循环引用 if(map.get(obj)){ return map.get(obj); } //初始化新变量 const type = getType(obj); let copy = getInit(obj); map.set(obj, copy); //克隆对象和数组 if(type === arrayTag || type === objectTag){ for(let key in obj){ if(obj.hasOwnProperty(key)){ copy[key] = deepClone(obj[key], map); } } } //克隆map,value也需要深拷贝 else if(type === mapTag){ obj.forEach((val, key) => { copy.set(key, deepClone(val, map)) }); } //克隆set,深拷贝每一个元素 else if(type === setTag){ obj.forEach(val => { copy.add(deepClone(val, map)) }) } return copy; }
继续考虑其他引用类型,这里只考虑Boolean, Number, String, Symbol 和 RegExp。
对于Symbol,由于一个程序中不能存在两个等值的Symbol,我们只能将其拷贝成一个Object:
function cloneSymbol(obj) { return Object(Symbol.prototype.valueOf.call(obj)); }
克隆正则:
function cloneReg(obj) { const reFlags = /\w*$/; //匹配在字符串末尾出现的0个或多个字母、数字、下划线,用于找修饰符 const copy = new RegExp(obj.source, reFlags.exec(obj)); copy.lastIndex = obj.lastIndex; //同步lastIndex return copy; }
Boolean, Number, String可以直接用构造函数克隆,合起来:
function cloneOtherTypes(obj, type){ const Constructor = obj.constructor; switch (type) { case boolTag: case numberTag: case stringTag: return new Constructor(obj); case symbolTag: return cloneSymbol(obj); case regTag: return cloneReg(obj); default: return null; } }
克隆函数
这里只简单的取出函数参数和函数体然后用new Function克隆。
匹配函数体的正则:/(?<={)(.|\n)*(?=})/m "{" 以后 "}" 以前 的 非换行符 或 换行符 ,多行匹配
匹配参数列表的正则:/(?<=\().+(?=\)\s*{)/ "(" 以后 ") }" 以前 的 非换行符
1. 使用toString将函数转化为字符串
2. 通过prototype判断是否为普通函数
3. 对普通函数,若有参数,首先将参数列表字符串转化成数组paramArray,然后调用new Function(...paramArr, body[0])生成新函数;
若没有参数,return new Function(body[0]);
4. 对箭头函数,return eval(funcString);
function cloneFunc(obj){ const bodyReg = /(?<={)(.|\n)*(?=})/m; const paramReg = /(?<=\()(.|\n)*(?=\)\s*{)/m; const funcString = obj.toString(); if(obj.prototype){ //普通函数 console.log('普通函数'); const body = bodyReg.exec(funcString); const param = paramReg.exec(funcString); if(param){ const paramArray = param[0].split(','); console.log('匹配到参数:', paramArray); console.log('body:', body[0]); return new Function(...paramArray, body[0]); }else{ return new Function(body[0]); } } else { //箭头函数 return eval(funcString); } }
最后,将所有函数合在一起:
//将各个引用类型名存储为常量,以便后续使用 const mapTag = getType(new Map()); const setTag = getType(new Set()); const arrayTag = getType(new Array()); const objectTag = getType(new Object()); const boolTag = getType(true); const stringTag = getType("1"); const numberTag = getType(1); const symbolTag = getType(Symbol()); const regTag = getType(new RegExp()); const deepTags = [mapTag, setTag, arrayTag, objectTag]; //判断是否需要递归拷贝,即是非null的引用类型 function isObject(variable){ return (variable != null) && (typeof variable == "object" || typeof variable == "function"); } //通过toString获取具体变量类型 function getType(obj){ return Object.prototype.toString.call(obj); } //获取构造函数并返回空对象 function getInit(obj){ const Constructor = obj.constructor; return new Constructor(); } //克隆Bool, Number, String, Symbol function cloneOtherTypes(obj, type){ const Constructor = obj.constructor; switch (type) { case boolTag: case numberTag: case stringTag: return new Constructor(obj); case symbolTag: return cloneSymbol(obj); case regTag: return cloneReg(obj); default: return null; } } //克隆Symbol function cloneSymbol(obj) { return Object(Symbol.prototype.valueOf.call(obj)); } //克隆正则 function cloneReg(obj) { const reFlags = /\w*$/; //匹配在字符串末尾出现的0个或多个字母、数字、下划线,用于找修饰符 const copy = new RegExp(obj.source, reFlags.exec(obj)); copy.lastIndex = obj.lastIndex; //同步lastIndex return copy; } //克隆函数 function cloneFunc(obj){ const bodyReg = /(?<={)(.|\n)*(?=})/m; const paramReg = /(?<=\()(.|\n)*(?=\)\s*{)/m; const funcString = obj.toString(); if(obj.prototype){ //普通函数 console.log('普通函数'); const body = bodyReg.exec(funcString); const param = paramReg.exec(funcString); if(param){ const paramArray = param[0].split(','); console.log('匹配到参数:', paramArray); console.log('body:', body[0]); return new Function(...paramArray, body[0]); }else{ return new Function(body[0]); } } else { //箭头函数 return eval(funcString); } } function deepClone(obj, map = new Map()){ //不需要递归拷贝,直接克隆值 if(!isObject(obj)){ return obj; } //防止循环引用 if(map.get(obj)){ return map.get(obj); } //初始化新变量 const type = getType(obj); let copy; if(deepTags.includes(type)){ copy = getInit(obj); } //记录已经克隆过的对象 map.set(obj, copy); //克隆对象和数组 if(type === arrayTag || type === objectTag){ for(let key in obj){ if(obj.hasOwnProperty(key)){ copy[key] = deepClone(obj[key], map); } } } //克隆函数 else if(typeof obj === "function"){ copy = cloneFunc(obj); } //克隆map,value也需要深拷贝 else if(type === mapTag){ obj.forEach((val, key) => { copy.set(key, deepClone(val, map)) }); } //克隆set,深拷贝每一个元素 else if(type === setTag){ obj.forEach(val => { copy.add(deepClone(val, map)) }) } //克隆其他类型 else{ copy = cloneOtherTypes(obj, type); } return copy; } const map = new Map(); map.set('key', 'value'); map.set('ConardLi', 'code秘密花园'); const set = new Set(); set.add('ConardLi'); set.add('code秘密花园'); const target = { field1: 1, field2: undefined, field3: { child: 'child' }, field4: [2, 4, 8], empty: null, map, set, bool: new Boolean(true), num: new Number(2), str: new String(2), symbol: Object(Symbol(1)), reg: /\d+/, func1: () => { console.log('code秘密花园'); }, func2: function (a, b) { return a + b; } }; console.log(deepClone(target));

浙公网安备 33010602011771号