作者:ConardLi
連接:https://juejin.im/post/6844903929705136141
來源:掘金
利用:
JSON.parse(JSON.stringify());
let obj = {
foo: 'foo'
}
let obj2 = JSON.parse(JSON.stringify(obj));
obj2.foo = 'bar'
console.log(obj);
console.log(obj2);
優點:
- 簡單,且可以應付,且可以應付很多應用場景
缺點 :
- 可能無法拷貝其他引用類型
- 無法拷貝函數
- 循環引用問題
先寫一個簡單的淺拷貝
function shallowCopy(obj) {
let targetObj = {}
for (const key in obj) {
targetObj[key] = obj[key]
}
return targetObj
}
我們可以透過遞歸(因為不知道對象總共有幾層)將上面的淺拷貝改寫成深拷貝
- 如果是原始類型,無需繼續拷貝,直接返回
- 如果是引用類型,創建一個新的對象,遍歷需要克隆的對象,將需要克隆對象的屬性執行深拷貝後依次添加到新對像上。
function deepCopy(target) {
if (typeof target === 'object') {
let cloneTarget = {};
for (const key in target) {
// 底下透過遞歸會一直往對象深層探勘
cloneTarget[key] = deepCopy(target[key]);
}
return cloneTarget;
} else {
return target;
}
};
const school = {
classA: {
stuA: {
name: 'Jack',
age: 15
},
stuB: {
name: 'Bill',
}
},
principal: 'Mike',
area: 22500,
isSchool: true
}
deepCopy(school)
可以說是一個最基本款的深拷貝,但是有一個問題是我們有考慮慮,如果target是數組呢?
只要在一開始定義的時候,多一個是不是數組的判斷即可
function deepCopy(target) {
if (typeof target === 'object') {
// let cloneTarget = {};
// 修改後的
// 判斷target是不是數組,是的話cloneTarget為[]不是的話為{}
let cloneTarget = Array.isArray(target) ? [] : {}
for (const key in target) {
// 底下透過遞歸會一直往對象深層探勘
cloneTarget[key] = deepCopy(target[key]);
}
return cloneTarget;
} else {
return target;
}
};
const school = {
classA: {
stuA: {
name: 'Jack',
age: 15
},
stuB: {
name: 'Bill',
hobbies: ['sleep', 'sing', 'basketball']
}
},
principal: 'Mike',
area: 22500,
isSchool: true
}
console.log(deepCopy(school))
先來看這個例子
const school = {
classA: {
stuA: {
name: 'Jack',
age: 15
},
stuB: {
name: 'Bill',
hobbies: ['sleep', 'sing', 'basketball']
}
},
principal: 'Mike',
area: 22500,
isSchool: true
}
school.foo = school
deepCopy(school)
這個主要原因是調用棧溢出(循環引用本身沒問題)
至於甚麼是調用棧溢出呢?可以參考底下這個問答串
https://stackoverflow.com/questions/6095530/maximum-call-stack-size-exceeded-error
創造一個儲存空間,用來存儲當前對象和拷貝對象的對應關係,當需要拷貝當前對象時,先去存儲空間中找,有沒有拷貝過這個對象,如果有的話( 代表有循環引用的情況發生 )直接返回,如果沒有的話繼續拷貝,這樣就巧妙化解的循環引用的問題。
function deepCopy(target, map = new Map()) {
if (typeof target === 'object') {
let cloneTarget = Array.isArray(target) ? [] : {};
for (const key in target) {
// Map會持續記錄拷貝過的對象(這裡傳入之前的map,因此不會默認再new Map)
cloneTarget[key] = deepCopy(target[key], map);
// 底下這幾行是為了預防循環引用這種情況發生
// 只要曾經拷貝過的對象都會被記錄
// 比方說這個例子,當我們遍歷到
// cloneTarget['foo'] = deepCopy(school['foo'], map);
// 因為school.foo引用的是自身(即是school),所以當我們調用deepCopy(school['foo'], map) 的時候,map.get(target)可以查找的到(cloneTarget自身)
// 因此代表的意義是
// cloneTarget['foo'] = map.get(school)
if (map.get(target)) {
return map.get(target);
}
map.set(target, cloneTarget);
console.log(cloneTarget);
}
return cloneTarget;
} else {
return target;
}
};
school = {
name: {
age: 19
}
}
school.foo = school
console.log(deepCopy(school));
先來看看甚麼是WeakMap
MDN官方解釋:
強引用弱引用?
詳情可以查看此篇文章
function deepCopy(target, map = new WeakMap()) {
...
}
上面我們僅考慮array以及普通的object,尚有其他引用類型需要我們考慮
除了引用類型還須考慮到這兩個範疇
// 返回的布林值代表是否為引用類型
function isObject(target) {
const type = typeof target;
return target !== null && (type === 'object' || type === 'function');
}
if (!isObject(target)) {
return target;
}
// ...
透過 Object.prototype.toString.call(target) 獲取較精準地引用類型
這裡封裝了一個判斷引用類型種類的函數
function getType(target) {
return Object.prototype.toString.call(target);
}
// 並將較常見的類型抓出來方便之後使用
const mapTag = '[object Map]';
const setTag = '[object Set]';
const arrayTag = '[object Array]';
const objectTag = '[object Object]';
// 這裡會需要列出numberTag主要是因為
// let numObj = new Number(123)
// numObj的類型會是對象
const boolTag = '[object Boolean]';
const dateTag = '[object Date]';
const errorTag = '[object Error]';
const numberTag = '[object Number]';
const stringTag = '[object String]';
const regexpTag = '[object RegExp]';
const symbolTag = '[object Symbol]';
這兩將分成兩大類來看
- 可以直接返回的類型
- 可以繼續克隆下去的類型
這裡只考慮四種類型 (還有更多可以遍歷的類型)
- Array
- Object
- Map
- Set
主要是改進這行,藉由 constructor
比方說創建一個對象除了可以
let obj = {} // 也可以這樣 let obj = new Object()
// 之前初始化數據
let cloneTarget = Array.isArray(target) ? [] : {};
// 這裡封裝一個函數可以透過constructor初始化
function getInit(target) {
const Ctor = target.constructor;
return new Ctor();
}
function clone(target, map = new WeakMap()) {
// 克隆原始类型
if (!isObject(target)) {
return target;
}
// 初始化
const type = getType(target);
let cloneTarget;
if (deepTag.includes(type)) {
cloneTarget = getInit(target, type);
}
// 防止循环引用
if (map.get(target)) {
return map.get(target);
}
map.set(target, cloneTarget);
// 克隆set
if (type === setTag) {
target.forEach(value => {
cloneTarget.add(clone(value,map));
});
return cloneTarget;
}
// 克隆map
if (type === mapTag) {
target.forEach((value, key) => {
cloneTarget.set(key, clone(value,map));
});
return cloneTarget;
}
// 克隆對象和數組
const keys = type === arrayTag ? undefined : Object.keys(target);
forEach(keys || target, (value, key) => {
if (keys) {
key = value;
}
cloneTarget[key] = clone(target[key], map);
});
return cloneTarget;
}
先處理可以透過構造函數以及數據創建的類型
const boolTag = '[object Boolean]';
const dateTag = '[object Date]';
const errorTag = '[object Error]';
const numberTag = '[object Number]';
const stringTag = '[object String]';
// 封裝一個函數處理這些
function cloneOtherType(target, type) {
const Ctor = target.constructor;
switch (type) {
case boolTag:
case numberTag:
case stringTag:
case errorTag:
case dateTag:
return new Ctor(target);
// 底下兩種因為比較特別,下面會需要繼續封裝各自的拷貝函數
case regexpTag:
return cloneReg(target);
case symbolTag:
return cloneSymbol(target);
default:
return null;
}
}
封裝Symbol以及正則的拷貝函數
// Symbol
function cloneSymbol(targe) {
return Object(Symbol.prototype.valueOf.call(targe));
}
// 正则:
function cloneReg(targe) {
const reFlags = /\w*$/;
const result = new targe.constructor(targe.source, reFlags.exec(targe));
result.lastIndex = targe.lastIndex;
return result;
}
基本上兩個對象引用同一個內存地址的函數其實沒啥關係
// loadash的代碼
const isFunc = typeof value == 'function'
if (isFunc || !cloneableTags[tag]) {
return object ? value : {}
}
但還是會希望可以再繼續克隆出一個新的函數,不然直接返回有點單調
思路:
- 透過prototype區分是否為箭頭函數(箭頭函數沒有)
- 如果是箭頭函數,則透過 eval 和函數字符串來重新生成一個箭頭函數
- 如果是普通函數則透過正則處理
function cloneFunction(func) {
const bodyReg = /(?<={)(.|\n)+(?=})/m;
const paramReg = /(?<=\().+(?=\)\s+{)/;
const funcString = func.toString();
if (func.prototype) {
console.log('普通函数');
const param = paramReg.exec(funcString);
const body = bodyReg.exec(funcString);
if (body) {
console.log('匹配到函数体:', body[0]);
if (param) {
const paramArr = param[0].split(',');
console.log('匹配到参数:', paramArr);
return new Function(...paramArr, body[0]);
} else {
return new Function(body[0]);
}
} else {
return null;
}
} else {
return eval(funcString);
}
}