基礎鞏固-你最少用幾行程式碼實現深拷貝?
點選上方 前端Q ,關注公眾號
回覆 加群 ,加入前端Q技術交流群
前言
深度克隆(深拷貝)一直都是初、中級前端面試中經常被問到的題目,網上介紹的實現方式也都各有千秋,大體可以概括為三種方式:
-
JSON.stringify+JSON.parse
, 這個很好理解; -
全量判斷型別,根據型別做不同的處理
-
2的變型,簡化型別判斷過程
前兩種比較常見也比較基礎,所以我們今天主要討論的是第三種。
閱讀全文你將學習到:
-
更簡潔的深度克隆方式
-
Object.getOwnPropertyDescriptors()
api -
型別判斷的通用方法
問題分析
深拷貝 自然是 相對 淺拷貝 而言的。我們都知道 引用資料型別 變數儲存的是資料的引用,就是一個指向記憶體空間的指標, 所以如果我們像賦值簡單資料型別那樣的方式賦值的話,其實只能複製一個指標引用,並沒有實現真正的資料克隆。
通過這個例子很容易就能理解:
const obj1 = { name: 'superman' } const obj2 = obj1; obj1.name = '前端切圖仔'; console.log(obj2.name); // 前端切圖仔 複製程式碼
所以深度克隆就是為了解決引用資料型別不能被通過賦值的方式 複製 的問題。
引用資料型別
我們不妨來羅列一下引用資料型別都有哪些:
-
ES6之前:Object, Array, Date, RegExp, Error,
-
ES6之後:Map, Set, WeakMap, WeakSet,
所以,我們要深度克隆,就需要對資料進行遍歷並根據型別採取相應的克隆方式。當然因為資料會存在多層巢狀的情況,採用 遞迴 是不錯的選擇。
簡單粗暴版本
function deepClone(obj) { let res = {}; // 型別判斷的通用方法 function getType(obj) { return Object.prototype.toString.call(obj).replaceAll(new RegExp(/\[|\]|object /g), ""); } const type = getType(obj); const reference = ["Set", "WeakSet", "Map", "WeakMap", "RegExp", "Date", "Error"]; if (type === "Object") { for (const key in obj) { if (Object.hasOwnProperty.call(obj, key)) { res[key] = deepClone(obj[key]); } } } else if (type === "Array") { console.log('array obj', obj); obj.forEach((e, i) => { res[i] = deepClone(e); }); } else if (type === "Date") { res = new Date(obj); } else if (type === "RegExp") { res = new RegExp(obj); } else if (type === "Map") { res = new Map(obj); } else if (type === "Set") { res = new Set(obj); } else if (type === "WeakMap") { res = new WeakMap(obj); } else if (type === "WeakSet") { res = new WeakSet(obj); }else if (type === "Error") { res = new Error(obj); } else { res = obj; } return res; } 複製程式碼
其實這就是我們最前面提到的第二種方式,很傻對不對,明眼人一眼就能看出來有很多冗餘程式碼可以合併。
我們先進行最基本的優化:
合併冗餘程式碼
將一眼就能看出來冗餘的程式碼合併下。
function deepClone(obj) { let res = null; // 型別判斷的通用方法 function getType(obj) { return Object.prototype.toString.call(obj).replaceAll(new RegExp(/\[|\]|object /g), ""); } const type = getType(obj); const reference = ["Set", "WeakSet", "Map", "WeakMap", "RegExp", "Date", "Error"]; if (type === "Object") { res = {}; for (const key in obj) { if (Object.hasOwnProperty.call(obj, key)) { res[key] = deepClone(obj[key]); } } } else if (type === "Array") { console.log('array obj', obj); res = []; obj.forEach((e, i) => { res[i] = deepClone(e); }); } // 優化此部分冗餘判斷 // else if (type === "Date") { // res = new Date(obj); // } else if (type === "RegExp") { // res = new RegExp(obj); // } else if (type === "Map") { // res = new Map(obj); // } else if (type === "Set") { // res = new Set(obj); // } else if (type === "WeakMap") { // res = new WeakMap(obj); // } else if (type === "WeakSet") { // res = new WeakSet(obj); // }else if (type === "Error") { // res = new Error(obj); //} else if (reference.includes(type)) { res = new obj.constructor(obj); } else { res = obj; } return res; } 複製程式碼
為了驗證程式碼的正確性,我們用下面這個資料驗證下:
const map = new Map(); map.set("key", "value"); map.set("ConardLi", "coder"); const set = new Set(); set.add("ConardLi"); set.add("coder"); 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)), date: new Date(), reg: /\d+/, error: new Error(), func1: () => { let t = 0; console.log("coder", t++); }, func2: function (a, b) { return a + b; }, }; //測試程式碼 const test1 = deepClone(target); target.field4.push(9); console.log('test1: ', test1); 複製程式碼
執行結果:
還有進一步優化的空間嗎?
答案當然是肯定的。
// 判斷型別的方法移到外部,避免遞迴過程中多次執行 const judgeType = origin => { return Object.prototype.toString.call(origin).replaceAll(new RegExp(/\[|\]|object /g), ""); }; const reference = ["Set", "WeakSet", "Map", "WeakMap", "RegExp", "Date", "Error"]; function deepClone(obj) { // 定義新的物件,最後返回 //通過 obj 的原型建立物件 const cloneObj = Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj)); // 遍歷物件,克隆屬性 for (let key of Reflect.ownKeys(obj)) { const val = obj[key]; const type = judgeType(val); if (reference.includes(type)) { newObj[key] = new val.constructor(val); } else if (typeof val === "object" && val !== null) { // 遞迴克隆 newObj[key] = deepClone(val); } else { // 基本資料型別和function newObj[key] = val; } } return newObj; } 複製程式碼
執行結果如下:
-
Object.getOwnPropertyDescriptors()
方法用來獲取一個物件的所有自身屬性的描述符。 -
返回所指定物件的所有自身屬性的描述符,如果沒有任何自身屬性,則返回空物件。
具體解釋和內容見 MDN [1]
這樣做的好處就是能夠提前定義好最後返回的資料型別。
這個實現參考了網上一位大佬的實現方式,個人覺得理解成本有點高,而且對陣列型別的處理也不是特別優雅, 返回類陣列。
我在我上面程式碼的基礎上進行了改造,改造後的程式碼如下:
function deepClone(obj) { let res = null; const reference = [Date, RegExp, Set, WeakSet, Map, WeakMap, Error]; if (reference.includes(obj?.constructor)) { res = new obj.constructor(obj); } else if (Array.isArray(obj)) { res = []; obj.forEach((e, i) => { res[i] = deepClone(e); }); } else if (typeof obj === "object" && obj !== null) { res = {}; for (const key in obj) { if (Object.hasOwnProperty.call(obj, key)) { res[key] = deepClone(obj[key]); } } } else { res = obj; } return res; } 複製程式碼
雖然程式碼量上沒有什麼優勢,但是整體的理解成本和你清晰度上我覺得會更好一點。那麼你覺得呢?
最後,還有迴圈引用問題,避免出現無線迴圈的問題。
我們用hash來儲存已經載入過的物件,如果已經存在的物件,就直接返回。
function deepClone(obj, hash = new WeakMap()) { if (hash.has(obj)) { return obj; } let res = null; const reference = [Date, RegExp, Set, WeakSet, Map, WeakMap, Error]; if (reference.includes(obj?.constructor)) { res = new obj.constructor(obj); } else if (Array.isArray(obj)) { res = []; obj.forEach((e, i) => { res[i] = deepClone(e); }); } else if (typeof obj === "object" && obj !== null) { res = {}; for (const key in obj) { if (Object.hasOwnProperty.call(obj, key)) { res[key] = deepClone(obj[key]); } } hash.set(obj, res); } else { res = obj; } return res; } 複製程式碼
總結
對於深拷貝的實現,可能存在很多不同的實現方式,關鍵在於理解其原理,並能夠記住一種最容易理解和實現的方式,面對類似的問題才能做到 臨危不亂,泰然自若 。上面的實現你覺得哪個更好呢?歡迎大佬們在評論區交流~
更文不易, 看完記得點個贊支援一下哦~ 這將是我寫作的動力源泉~
關於本文
作者:前端superman
http://juejin.cn/post/7075351322014253064
往期推薦
最後
-
歡迎加我微信,拉你進技術群,長期交流學習...
-
歡迎關注「前端Q」,認真學前端,做個專業的技術人...
點個 在看 支援我吧
- package.json 與 package-lock.json 的關係
- 用TypeORM還需要手動釋放資料庫連線嗎?
- TypeORM中更新資料庫的坑
- 12 個適合做外包專案的開源後臺管理系統
- 基礎鞏固-你最少用幾行程式碼實現深拷貝?
- 基於 GraphQL 的 BFF 層技術演進之路
- 阿里前端:我的老婆失業了,周圍同事也在不斷被裁
- 2022,VSCode 前端外掛推薦
- 愛奇藝低程式碼引擎:千變萬化、快速搭建的萬花筒
- 產品:“你前端,寫個 banner 這麼費勁?”
- 手把手教你如何把小程式裝進自己的APP
- 前端切入全棧不要只盯著MongoDB,SQL才是YYDS
- Web 框架的替代方案
- Sequelize一對一、一對多、多對多學不會你砍我
- 10 個 Node.js 最佳實踐:來自 Node 專家的啟示
- 2022網際網路大廠薪資大比拼
- 面試官 -- 跨域請求如何攜帶cookie?
- 給力!快速瞭解Rust 模組使用方式
- Async是如何被 JavaScript 實現的
- 尤大說要學習的VUE新書來了!(免費送4本)