# 實現深克隆
點擊關注本[公眾號](http://www.hmoore.net/book/dsh225/javascript_vue_css/edit#_118)獲取文檔最新更新,并可以領取配套于本指南的《**前端面試手冊**》以及**最標準的簡歷模板**.
[TOC]
## 前言
實現一個深克隆是面試中常見的問題的,可是絕大多數面試者的答案都是不完整的,甚至是錯誤的,這個時候面試官會不斷追問,看看你到底理解不理解深克隆的原理,很多情況下一些一知半解的面試者就原形畢漏了.
我們就來看一下如何實現一個深克隆,當然面試中沒有讓你完整實現的時候,但是你一定要搞清楚其中的坑在哪里,才可以輕松應對面試官的追問.
* JavaScript原始類型: Undefined、Null、Boolean、Number、String、Symbol
* JavaScript引用類型:Object
## 淺克隆
**淺克隆**之所以被稱為**淺克隆**,是因為對象只會被克隆最外部的一層,至于更深層的對象,則依然是通過引用指向同一塊堆內存.
~~~
// 淺克隆函數
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`也被改變了,這就是淺克隆的問題所在.
當然有一個新的api`Object.assign()`也可以實現淺復制,但是效果跟上面沒有差別,所以我們不再細說了.
## 深克隆
### 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
~~~
對于正則對象,我們在處理之前要先補充一點新知識.
我們需要通過[正則的擴展](http://es6.ruanyifeng.com/#docs/regex#flags-%E5%B1%9E%E6%80%A7)了解到`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`的深克隆實現.
在面試過程中,我們上面提到的眾多坑是面試官很可能追問你的,要知道坑在哪里,能答出來才是你的加分項,在面試過程中必須要有一兩個閃光點,如果只知道**序列/反序列**這種投機取巧的方法,在追問下不僅拿不到分,很可能造成只懂個皮毛的印象,畢竟,面試面得就是你知識的深度.
* * *
## 公眾號
想要實時關注筆者最新的文章和最新的文檔更新請關注公眾號**程序員面試官**,后續的文章會優先在公眾號更新.
**簡歷模板**:關注公眾號回復「模板」獲取
**《前端面試手冊》**:配套于本指南的突擊手冊,關注公眾號回復「fed」獲取

- 前言
- 指南使用手冊
- 為什么會有這個項目
- 面試技巧
- 面試官到底想看什么樣的簡歷?
- 面試回答問題的技巧
- 如何通過HR面
- 推薦
- 書籍/課程推薦
- 前端基礎
- HTML基礎
- CSS基礎
- JavaScript基礎
- 瀏覽器與新技術
- DOM
- 前端基礎筆試
- HTTP筆試部分
- JavaScript筆試部分
- 前端原理詳解
- JavaScript的『預解釋』與『變量提升』
- Event Loop詳解
- 實現不可變數據
- JavaScript內存管理
- 實現深克隆
- 如何實現一個Event
- JavaScript的運行機制
- 計算機基礎
- HTTP協議
- TCP面試題
- 進程與線程
- 數據結構與算法
- 算法面試題
- 字符串類面試題
- 前端框架
- 關于前端框架的面試須知
- Vue面試題
- React面試題
- 框架原理詳解
- 虛擬DOM原理
- Proxy比defineproperty優劣對比?
- setState到底是異步的還是同步的?
- 前端路由的實現
- redux原理全解
- React Fiber 架構解析
- React組件復用指南
- React-hooks 抽象組件
- 框架實戰技巧
- 如何搭建一個組件庫的開發環境
- 組件設計原則
- 實現輪播圖組件
- 性能優化
- 前端性能優化-加載篇
- 前端性能優化-執行篇
- 工程化
- webpack面試題
- 前端工程化
- Vite
- 安全
- 前端安全面試題
- npm
- 工程化原理
- 如何寫一個babel
- Webpack HMR 原理解析
- webpack插件編寫
- webpack 插件化設計
- Webpack 模塊機制
- webpack loader實現
- 如何開發Babel插件
- git
- 比較
- 查看遠程倉庫地址
- git flow
- 比較分支的不同并保存壓縮文件
- Tag
- 回退
- 前端項目經驗
- 確定用戶是否在當前頁面
- 前端下載文件
- 只能在微信中訪問
- 打開新頁面-被瀏覽器攔截
- textarea高度隨內容變化 vue版
- 去掉ios原始播放大按鈕
- nginx在MAC上的安裝、啟動、重啟和關閉
- 解析latex格式的數學公式
- 正則-格式化a鏈接
- 封裝的JQ插件庫
- 打包問題總結
- NPM UI插件
- 帶你入門前端工程
- webWorker+indexedDB性能優化
- 多個相鄰元素切換效果出現邊框重疊問題的解決方法
- 監聽前端storage變化