摘要:深拷貝與淺拷貝的出現,就與這兩個數據類型有關。這時,就需要用淺拷貝來實現了。數據一但過多,就會有遞歸爆棧的風險。這個方法是在解決遞歸爆棧問題的基礎上,加以改進解決循環引用的問題。但如果你并不想保持引用,那就改用用于解決遞歸爆棧即可。
前言
這是前端面試題系列的第 9 篇,你可能錯過了前面的篇章,可以在這里找到:
數組去重(10 種濃縮版)
JavaScript 中的事件機制(從原生到框架)
理解函數的柯里化
ES6 中箭頭函數的用法
this 的原理以及用法
偽類與偽元素的區別及實戰
如何實現一個圣杯布局?
今日頭條 面試題和思路解析
面試的時候,我經常會問候選人深拷貝與淺拷貝的問題。因為它可以考察一個人的很多方面,比如基本功,邏輯能力,編碼能力等等。
另外在實際工作中,也常會遇到它。比如用于頁面展示的數據狀態,與需要傳給后端的數據包中,有部分字段的值不一致的話,就需要在傳參時根據接口文檔覆寫那幾個字段的值。
最常見的可能就是 status 這個參數了。界面上的展示需要 Boolean 值,而后端同學希望拿到的是 Number 值,1 或者 0。為了不影響展示效果,往往就需要深拷貝一下,再進行覆寫,否則界面上就會因為某些值的變化,出現奇怪的現象。
至于為什么會這樣,下文會講到。馬上開始今天的主題,讓我們先從賦值開始說起。
賦值Javascript 的原始數據類型有這幾種:Boolean、Null、Undefined、Number、String、Symbol(ES6)。它們的賦值很簡單,且賦值后兩個變量互不影響。
let test1 = "chao"; let test2 = test1; // test2: chao test1 = "chao_change"; // test2: chao // test1: chao_change
另外的引用數據類型有:Object 和 Array。深拷貝與淺拷貝的出現,就與這兩個數據類型有關。
const obj = {a:1, b:2}; const obj2 = obj; obj2.a = 3; console.log(obj.a); // 3
依照賦值的思路,對 Object 引用類型進行拷貝,就會出問題。很多情況下,這不是我們想要的。這時,就需要用淺拷貝來實現了。
淺拷貝什么是淺拷貝?可以這么理解:創建一個新的對象,把原有的對象屬性值,完整地拷貝過來。其中包括了原始類型的值,還有引用類型的內存地址。
讓我們用 Object.assign 來改寫一下上面的例子:
const obj = {a:1, b:2}; const obj2 = Object.assign({}, obj); obj2.a = 3; console.log(obj.a); // 1
Ok,改變了 obj2 的 a 屬性,但 obj 的 a 并沒有發生變化,這正是我們想要的。
可是,這樣的拷貝還有瑕疵,再改一下例子:
const arr = [{a:1,b:2}, {a:3,b:4}]; const newArr = [].concat(arr); newArr.length = 1; // 為了方便區分,只保留新數組的第一個元素 console.log(newArr); // [{a:1,b:2}] console.log(arr); // [{a:1,b:2},{a:3,b:4}] newArr[0].a = 123; // 修改 newArr 中第一個元素的a console.log(arr[0]); // {a: 123, b: 2},竟然把 arr 的第一個元素的 a 也改了
oh,no!這不是我們想要的...
經過一番查找,才發現:原來,對象的 Object.assign(),數組的 Array.prototype.slice() 和 Array.prototype.concat(),還有 ES6 的 擴展運算符,都有類似的問題,它們都屬于 淺拷貝。這一點,在實際工作中處理數據的組裝時,要格外注意。
所以,我將淺拷貝這樣定義:只拷貝第一層的原始類型值,和第一層的引用類型地址。
深拷貝我們當然希望當拷貝多層級的對象時,也能實現互不影響的效果。所以,深拷貝的概念也就油然而生了。我將深拷貝定義為:拷貝所有的屬性值,以及屬性地址指向的值的內存空間。
也就是說,當遇到對象時,就再新開一個對象,然后將第二層源對象的屬性值,完整地拷貝到這個新開的對象中。
按照淺拷貝的思路,很容易就想到了遞歸調用。所以,就自己封裝了個深拷貝的方法:
function deepClone(obj) { if(!obj && typeof obj !== "object"){ return; } var newObj= toString.call(obj) === "[object Array]" ? [] : {}; for (var key in obj) { if (obj[key] && typeof obj[key] === "object") { newObj[key] = deepClone(obj[key]); } else { newObj[key] = obj[key]; } } return newObj; }
再試試看:
let arr = [{a:1,b:2}, {a:3,b:4}]; let newArr = deepClone(arr); newArr.length = 1; // 為了方便區分,只保留新數組的第一個元素 console.log(newArr); // [{a:1, b:2}] console.log(arr); // [{a:1, b:2}, {a:3, b:4}] newArr[0].a = 123; // 修改 newArr 中第一個元素的 a console.log(arr[0]); // {a:1, b:2}
ok,這下搞定了。
不過,這個方法貌似會存在 引用丟失 的的問題。比如這樣:
var b = {}; var a = {a1: b, a2: b}; a.a1 === a.a2 // true var c = clone(a); c.a1 === c.a2 // false
如果我們的需求是,應該丟失引用,那就可以用這個方法。反之,就得想辦法解決。
一行代碼的深拷貝當然,還有最簡單粗暴的深拷貝方法,就是利用 JSON 了。像這樣:
let newArr2 = JSON.parse(JSON.stringify(arr)); console.log(arr[0]); // {a:1, b:2} newArr2[0].a = 123; console.log(arr[0]); // {a:1, b:2}
但是,JSON 內部用了遞歸的方式。數據一但過多,就會有遞歸爆棧的風險。
// Maximum call stack size exceeded深拷貝的終極方案
有位大佬給出了深拷貝的終極方案,利用了“棧”的思想。
function cloneForce(x) { // 用來去重 const uniqueList = []; let root = {}; // 循環數組 const loopList = [ { parent: root, key: undefined, data: x, } ]; while(loopList.length) { // 深度優先 const node = loopList.pop(); const parent = node.parent; const key = node.key; const data = node.data; // 初始化賦值目標,key為undefined則拷貝到父元素,否則拷貝到子元素 let res = parent; if (typeof key !== "undefined") { res = parent[key] = {}; } // 數據已經存在 let uniqueData = uniqueList.find((item) => item.source === data ); if (uniqueData) { parent[key] = uniqueData.target; // 中斷本次循環 continue; } // 數據不存在 // 保存源數據,在拷貝數據中對應的引用 uniqueList.push({ source: data, target: res, }); for(let k in data) { if (data.hasOwnProperty(k)) { if (typeof data[k] === "object") { // 下一次循環 loopList.push({ parent: res, key: k, data: data[k], }); } else { res[k] = data[k]; } } } } return root; }
其思路是:引入一個數組 uniqueList 用來存儲已經拷貝的數組,每次循環遍歷時,先判斷對象是否在 uniqueList 中了,如果在的話就不執行拷貝邏輯了。
這個方法是在解決遞歸爆棧問題的基礎上,加以改進解決循環引用的問題。但如果你并不想保持引用,那就改用 cloneLoop(用于解決遞歸爆棧)即可。有興趣的同學,可以前往 深拷貝的終極探索(90%的人都不知道),查看更多的細節。
總結所謂深拷貝與淺拷貝,指的是 Object 和 Array 這樣的引用數據類型。
淺拷貝,只拷貝第一層的原始類型值,和第一層的引用類型地址。
深拷貝,拷貝所有的屬性值,以及屬性地址指向的值的內存空間。通過遞歸調用,或者 JSON 來做深拷貝,都會有一些問題。而 cloneForce 方法倒是目前看來最完美的解決方案了。
在日常的工作中,我們要特別注意,對象的 Object.assign(),數組的 Array.prototype.slice() 和 Array.prototype.concat(),還有 ES6 的 擴展運算符,都屬于淺拷貝。當需要做數據組裝時,一定要用深拷貝,以免影響界面展示效果。
崗位內推莉莉絲游戲招 中高級前端工程師 啦!!!
你玩過《小冰冰傳奇([刀塔傳奇])》么?你玩過《劍與家園》么?
你想和 薛兆豐老師 成為同事么?有興趣的同學,可以 關注下面的公眾 號加我微信 詳聊哈~
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/103559.html
摘要:上對位運算的解釋是它經常被用來創建處理以及讀取標志位序列一種類似二進制的變量。位運算,常用于處理同時存在多個布爾選項的情形。掩碼中的每個選項的值都是的冪,位運算是位的。位運算,說白了就是直接對某個數據在內存中的二進制位,進行運算操作。 showImg(https://segmentfault.com/img/bVbrC56?w=2208&h=1242); 前言 上一篇文章 「前端面試題...
摘要:前言前兩天總結了一下方面的面試題傳送門,今天翻看了一些面試中常見的幾個問題只是一部分,會持續更新,分享給有需要的小伙伴,歡迎關注如果文章中有出現紕漏錯誤之處,還請看到的小伙伴留言指正,先行謝過以下有哪些數據類型種原始數據類型布爾表示一個邏輯 前言 前兩天總結了一下HTML+CSS方面的面試題 (傳送門),今天翻看了一些 JavaScript 面試中常見的幾個問題(只是一部分,會持續更新...
摘要:面試的公司分別是阿里網易滴滴今日頭條有贊挖財滬江餓了么攜程喜馬拉雅兌吧微醫寺庫寶寶樹海康威視蘑菇街酷家樂百分點和海風教育。 (關注福利,關注本公眾號回復[資料]領取優質前端視頻,包括Vue、React、Node源碼和實戰、面試指導) 本人于7-8月開始準備面試,過五關斬六將,最終抱得網易歸,深深感受到高級前端面試的套路。以下是自己整理的面試題匯總,不敢藏私,統統貢獻出來。 面試的公司分...
摘要:用于檢測自己是否在自己的原型鏈上如果是函數,則取出該函數的原型對象否則,取出對象的原型對象其中,的判斷,是為了確定的類型是對象或數組。相當于,而的構造函數是一個函數對象。 showImg(https://segmentfault.com/img/bVbq2N1?w=640&h=437); 前言 接著上一篇文章 lodash 是如何實現深拷貝的(上),今天會繼續解讀 _.cloneDee...
閱讀 3093·2021-11-22 09:34
閱讀 592·2021-11-22 09:34
閱讀 2436·2021-10-08 10:18
閱讀 3372·2021-09-22 15:57
閱讀 2585·2021-09-22 15:25
閱讀 2398·2019-08-30 15:54
閱讀 2092·2019-08-30 15:44
閱讀 1798·2019-08-29 11:18