面試官系列(2):既然React/Vue可以用Event Bus進行元件通訊,你可以實現下嗎?
前言
實現一個深克隆是面試中常見的問題的,可是絕大多數面試者的答案都是不完整的,甚至是錯誤的,這個時候面試官會不斷追問,看看你到底理解不理解深克隆的原理,很多情況下一些一知半解的面試者就原形畢漏了.
我們就來看一下如何實現一個深克隆,當然面試中沒有讓你完整實現的時候,但是你一定要搞清楚其中的坑在哪裡,才可以輕鬆應對面試官的追問.
在要實現一個深克隆之前我們需要了解一下javascript中的基礎型別.
JavaScript原始型別:Undefined、Null、Boolean、Number、String、Symbol
JavaScript引用型別:Object
1.淺克隆
淺克隆之所以被稱為淺克隆,是因為物件只會被克隆最外部的一層,至於更深層的物件,則依然是通過引用指向同一塊堆記憶體.
// 淺克隆函式
function shallowClone(o) {
const obj = {};
for ( let i in o) {
obj[i] = o[i];
}
return obj;
}
// 被克隆物件
const oldObj = {
a: 1,
b: [ 'e', 'f', 'g' ],
c: { h: { i: 2 } }
};
const newObj = shallowClone(oldObj);
console.log(newObj.c.h, oldObj.c.h); // { i: 2 } { i: 2 }
console.log(oldObj.c.h === newObj.c.h); // true
複製程式碼
我們可以看到,很明顯雖然oldObj.c.h
被克隆了,但是它還與oldObj.c.h
相等,這表明他們依然指向同一段堆記憶體,這就造成了如果對newObj.c.h
進行修改,也會影響oldObj.c.h
,這就不是一版好的克隆.
newObj.c.h.i = 'change';
console.log(newObj.c.h, oldObj.c.h); // { i: 'change' } { i: 'change' }
複製程式碼
我們改變了newObj.c.h.i
的值,oldObj.c.h.i
也被改變了,這就是淺克隆的問題所在.
當然有一個新的apiObject.assign()
也可以實現淺複製,但是效果跟上面沒有差別,所以我們不再細說了.
2.深克隆
2.1 JSON.parse方法
前幾年微博上流傳著一個傳說中最便捷實現深克隆的方法, JSON物件parse方法可以將JSON字串反序列化成JS物件,stringify方法可以將JS物件序列化成JSON字串,這兩個方法結合起來就能產生一個便捷的深克隆.
const newObj = JSON.parse(JSON.stringify(oldObj));
複製程式碼
我們依然用上一節的例子進行測試
const oldObj = {
a: 1,
b: [ 'e', 'f', 'g' ],
c: { h: { i: 2 } }
};
const newObj = JSON.parse(JSON.stringify(oldObj));
console.log(newObj.c.h, oldObj.c.h); // { i: 2 } { i: 2 }
console.log(oldObj.c.h === newObj.c.h); // false
newObj.c.h.i = 'change';
console.log(newObj.c.h, oldObj.c.h); // { i: 'change' } { i: 2 }
複製程式碼
果然,這是一個實現深克隆的好方法,但是這個解決辦法是不是太過簡單了.
確實,這個方法雖然可以解決絕大部分是使用場景,但是卻有很多坑.
1.他無法實現對函式 、RegExp等特殊物件的克隆
2.會拋棄物件的constructor,所有的建構函式會指向Object
3.物件有迴圈引用,會報錯
主要的坑就是以上幾點,我們一一測試下.
// 建構函式
function person(pname) {
this.name = pname;
}
const Messi = new person('Messi');
// 函式
function say() {
console.log('hi');
};
const oldObj = {
a: say,
b: new Array(1),
c: new RegExp('ab+c', 'i'),
d: Messi
};
const newObj = JSON.parse(JSON.stringify(oldObj));
// 無法複製函式
console.log(newObj.a, oldObj.a); // undefined [Function: say]
// 稀疏陣列複製錯誤
console.log(newObj.b[0], oldObj.b[0]); // null undefined
// 無法複製正則物件
console.log(newObj.c, oldObj.c); // {} /ab+c/i
// 建構函式指向錯誤
console.log(newObj.d.constructor, oldObj.d.constructor); // [Function: Object] [Function: person]
複製程式碼
我們可以看到在對函式、正則物件、稀疏陣列等物件克隆時會發生意外,建構函式指向也會發生錯誤。
const oldObj = {};
oldObj.a = oldObj;
const newObj = JSON.parse(JSON.stringify(oldObj));
console.log(newObj.a, oldObj.a); // TypeError: Converting circular structure to JSON
複製程式碼
物件的迴圈引用會丟擲錯誤.
2.2 構造一個深克隆函式
我們知道要想實現一個靠譜的深克隆方法,上一節提到的序列/反序列是不可能了,而通常教程裡提到的方法也是不靠譜的,他們存在的問題跟上一屆序列反序列操作中凸顯的問題是一致的.
(這個方法也會出現上一節提到的問題)由於要面對不同的物件(正則、陣列、Date等)要採用不同的處理方式,我們需要實現一個物件型別判斷函式。
const isType = (obj, type) => {
if (typeof obj !== 'object') return false;
const typeString = Object.prototype.toString.call(obj);
let flag;
switch (type) {
case 'Array':
flag = typeString === '[object Array]';
break;
case 'Date':
flag = typeString === '[object Date]';
break;
case 'RegExp':
flag = typeString === '[object RegExp]';
break;
default:
flag = false;
}
return flag;
};
複製程式碼
這樣我們就可以對特殊物件進行型別判斷了,從而採用針對性的克隆策略.
const arr = Array.of(3, 4, 5, 2);
console.log(isType(arr, 'Array')); // true
複製程式碼
對於正則物件,我們在處理之前要先補充一點新知識.
我們需要通過正則的擴充套件瞭解到flags
屬性等等,因此我們需要實現一個提取flags的函式.
const getRegExp = re => {
var flags = '';
if (re.global) flags += 'g';
if (re.ignoreCase) flags += 'i';
if (re.multiline) flags += 'm';
return flags;
};
複製程式碼
做好了這些準備工作,我們就可以進行深克隆的實現了.
/**
* deep clone
* @param {[type]} parent object 需要進行克隆的物件
* @return {[type]} 深克隆後的物件
*/
const clone = parent => {
// 維護兩個儲存迴圈引用的陣列
const parents = [];
const children = [];
const _clone = parent => {
if (parent === null) return null;
if (typeof parent !== 'object') return parent;
let child, proto;
if (isType(parent, 'Array')) {
// 對陣列做特殊處理
child = [];
} else if (isType(parent, 'RegExp')) {
// 對正則物件做特殊處理
child = new RegExp(parent.source, getRegExp(parent));
if (parent.lastIndex) child.lastIndex = parent.lastIndex;
} else if (isType(parent, 'Date')) {
// 對Date物件做特殊處理
child = new Date(parent.getTime());
} else {
// 處理物件原型
proto = Object.getPrototypeOf(parent);
// 利用Object.create切斷原型鏈
child = Object.create(proto);
}
// 處理迴圈引用
const index = parents.indexOf(parent);
if (index != -1) {
// 如果父陣列存在本物件,說明之前已經被引用過,直接返回此物件
return children[index];
}
parents.push(parent);
children.push(child);
for (let i in parent) {
// 遞迴
child[i] = _clone(parent[i]);
}
return child;
};
return _clone(parent);
};
複製程式碼
我們做一下測試
function person(pname) {
this.name = pname;
}
const Messi = new person('Messi');
function say() {
console.log('hi');
}
const oldObj = {
a: say,
c: new RegExp('ab+c', 'i'),
d: Messi,
};
oldObj.b = oldObj;
const newObj = clone(oldObj);
console.log(newObj.a, oldObj.a); // [Function: say] [Function: say]
console.log(newObj.b, oldObj.b); // { a: [Function: say], c: /ab+c/i, d: person { name: 'Messi' }, b: [Circular] } { a: [Function: say], c: /ab+c/i, d: person { name: 'Messi' }, b: [Circular] }
console.log(newObj.c, oldObj.c); // /ab+c/i /ab+c/i
console.log(newObj.d.constructor, oldObj.d.constructor); // [Function: person] [Function: person]
複製程式碼
當然,我們這個深克隆還不算完美,例如Buffer物件、Promise、Set、Map可能都需要我們做特殊處理,另外對於確保沒有迴圈引用的物件,我們可以省去對迴圈引用的特殊處理,因為這很消耗時間,不過一個基本的深克隆函式我們已經實現了。
總結
實現一個完整的深克隆是由許多坑要踩的,npm上一些庫的實現也不夠完整,在生產環境中最好用lodash
的深克隆實現.
在面試過程中,我們上面提到的眾多坑是面試官很可能追問你的,要知道坑在哪裡,能答出來才是你的加分項,在面試過程中必須要有一兩個閃光點,如果只知道序列/反序列這種投機取巧的方法,在追問下不僅拿不到分,很可能造成只懂個皮毛的印象,畢竟,面試面得就是你知識的深度.