摘要:引用類型之所以會(huì)出現(xiàn)深淺拷貝的問題,實(shí)質(zhì)上是由于對(duì)基本類型和引用類型的處理不同。另外方法可以視為數(shù)組對(duì)象的淺拷貝。上面描述過的復(fù)雜問題依然存在,可以說是最簡(jiǎn)陋但是日常工作夠用的深拷貝方式。
一直想梳理下工作中經(jīng)常會(huì)用到的深拷貝的內(nèi)容,然而遍覽了許多的文章,卻發(fā)現(xiàn)對(duì)深拷貝并沒有一個(gè)通用的完美實(shí)現(xiàn)方式。因?yàn)閷?duì)深拷貝的定義不同,實(shí)現(xiàn)時(shí)的edge case過多,在深拷貝的時(shí)候會(huì)出現(xiàn)循環(huán)引用等問題,導(dǎo)致JS內(nèi)部并沒有實(shí)現(xiàn)深拷貝,但是我們可以來探究一下深拷貝到底有多復(fù)雜,各種實(shí)現(xiàn)方式的優(yōu)缺點(diǎn),同時(shí)參考下常用庫對(duì)其的實(shí)現(xiàn)。
引用類型之所以會(huì)出現(xiàn)深淺拷貝的問題,實(shí)質(zhì)上是由于JS對(duì)基本類型和引用類型的處理不同?;绢愋椭傅氖呛?jiǎn)單的數(shù)據(jù)段,而引用類型指的是一個(gè)對(duì)象,而JS不允許我們直接操作內(nèi)存中的地址,也就是不能操作對(duì)象的內(nèi)存空間,所以,我們對(duì)對(duì)象的操作都只是在操作它的引用而已。
在復(fù)制時(shí)也是一樣,如果我們復(fù)制一個(gè)基本類型的值時(shí),會(huì)創(chuàng)建一個(gè)新值,并把它保存在新的變量的位置上。而如果我們復(fù)制一個(gè)引用類型時(shí),同樣會(huì)把變量中的值復(fù)制一份放到新的變量空間里,但此時(shí)復(fù)制的東西并不是對(duì)象本身,而是指向該對(duì)象的指針。所以我們復(fù)制引用類型后,兩個(gè)變量其實(shí)指向同一個(gè)對(duì)象,改變其中一個(gè)對(duì)象,會(huì)影響到另外一個(gè)。
var num = 10; var obj = { name: "Nicholas" } var num2 = num; var obj2 = obj; obj.name = "Lee"; obj2.name; // "Lee"
可以看到我們的obj和obj2都保存了一個(gè)指向該對(duì)象的指針,所有的操作都是對(duì)該引用的操作,所以對(duì)對(duì)象的修改會(huì)影響其他的復(fù)制對(duì)象。
淺拷貝如果我們要復(fù)制對(duì)象的所有屬性都不是引用類型時(shí),就可以使用淺拷貝,實(shí)現(xiàn)方式就是遍歷并復(fù)制,最后返回新的對(duì)象。
function shallowCopy(obj) { var copy = {}; // 只復(fù)制可遍歷的屬性 for (key in obj) { // 只復(fù)制本身擁有的屬性 if (obj.hasOwnProperty(key)) { copy[key] = obj[key]; } } return copy; }
如上面所說,我們使用淺拷貝會(huì)復(fù)制所有引用對(duì)象的指針,而不是具體的值,所以使用時(shí)一定要明確自己的需求,同時(shí),淺拷貝的實(shí)現(xiàn)也是最簡(jiǎn)單的。
JS內(nèi)部實(shí)現(xiàn)了淺拷貝,如Object.assign(),其中第一個(gè)參數(shù)是我們最終復(fù)制的目標(biāo)對(duì)象,后面的所有參數(shù)是我們的即將復(fù)制的源對(duì)象,支持對(duì)象或數(shù)組,一般調(diào)用的方式為
var newObj = Object.assign({}, originObj);
這樣我們就得到了一個(gè)新的淺拷貝對(duì)象。另外[].slice()方法可以視為數(shù)組對(duì)象的淺拷貝。
深拷貝如果我們需要復(fù)制一個(gè)擁有所有屬性和方法的新對(duì)象,就要用到深拷貝,JS并沒有內(nèi)置深拷貝方法,主要是因?yàn)椋?/p>
深拷貝怎么定義?我們?cè)趺刺幚碓??怎么區(qū)分可拷貝的對(duì)象?原生DOM/BOM對(duì)象怎么拷貝?函數(shù)是新建還是引用?這些edge case太多導(dǎo)致我們無法統(tǒng)一概念,造出大家都滿意的深拷貝方法來。
內(nèi)部循環(huán)引用怎么處理,是不是保存每個(gè)遍歷過的對(duì)象列表,每次進(jìn)行對(duì)比,然后再造一個(gè)循環(huán)引用來?這樣帶來的性能消耗可以接受嗎。
解釋一些常見的問題概念,防止有些同學(xué)不明白我們?cè)谥v什么。比如循環(huán)引用:
var obj = {}; obj.b = obj;
這樣當(dāng)我們深拷貝obj對(duì)象時(shí),就會(huì)循環(huán)的遍歷b屬性,直到棧溢出。
我們的解決方案為建立一個(gè)集合[],每次遍歷對(duì)象進(jìn)行比較,如果[]中已存在,則證明出現(xiàn)了循環(huán)引用或者相同引用,我們直接返回該對(duì)象已復(fù)制的引用即可:
let hasObj = []; function referCopy(obj) { let copy = {}; hasObj.push(obj); for (let i in obj) { if (typeof obj[i] === "object") { let index = hasObj.indexOf(obj[i]); if (index > -1) { console.log("存在循環(huán)引用或?qū)傩砸昧讼嗤瑢?duì)象"); // 如果已存在,證明引用了相同對(duì)象,那么無論是循環(huán)引用還是重復(fù)引用,我們返回引用就可以了 copy[i] = hasObj[index]; } else { copy[i] = referCopy(obj[i]); } } else { copy[i] = obj[i]; } } return copy; }
處理原型和區(qū)分可拷貝的對(duì)象:我們一般使用function.prototype指代原型,使用obj.__proto__指代原型鏈,使用enumerable屬性表示是否可以被for ... in等遍歷,使用hasOwnProperty來查詢是否是本身元素。在原型鏈和可遍歷屬性和自身屬性之間存在交集,但都不相等,我們應(yīng)該如何判斷哪些屬性應(yīng)該被復(fù)制呢?
函數(shù)的處理:函數(shù)擁有一些內(nèi)在屬性,但我們一般不修改這些屬性,所以函數(shù)一般直接引用其地址即可。但是擁有一些存取器屬性的函數(shù)我們?cè)趺刺幚恚渴菑?fù)制值還是復(fù)制存取描述符?
var obj = { age: 10, get age() { return this.age; }, set age(age) { this.age = age; } }; var obj2 = $.extend(true, {}, obj); obj2; // {age: 10}
這個(gè)是我們想要的結(jié)果嗎?大部分場(chǎng)景下不是吧,比如我要復(fù)制一個(gè)已有的Vue對(duì)象。當(dāng)然我們也有解決方案:
function copy(obj) { var copy = {}; for (var i in obj) { let desc = Object.getOwnPropertyDescriptor(obj, i); // 檢測(cè)是否為存取描述符 if (desc.set || desc.get) { Object.defineProperty(copy, i, { get: desc.get, set: desc.set, configuarable: desc.configuarable, enumerable: true }); // 否則為數(shù)據(jù)描述符,則復(fù)用下面的深拷貝方法,此處簡(jiǎn)寫 } else { copy[i] = obj[i]; } } return copy; }
雖然邊界條件很多,但是不同的框架和庫都對(duì)該方法進(jìn)行了實(shí)現(xiàn),只不過定義不同,實(shí)現(xiàn)方式也不同,如jQuery.extend()只復(fù)制可枚舉的屬性,不繼承原型鏈,函數(shù)復(fù)制引用,內(nèi)部循環(huán)引用不處理。而lodash實(shí)現(xiàn)的就更為優(yōu)秀,它實(shí)現(xiàn)了結(jié)構(gòu)化克隆算法。
該算法的優(yōu)點(diǎn)是:
可以復(fù)制 RegExp 對(duì)象。
可以復(fù)制 Blob、File 以及 FileList 對(duì)象。
可以復(fù)制 ImageData 對(duì)象。CanvasPixelArray 的克隆粒度將會(huì)跟原始對(duì)象相同,并且復(fù)制出來相同的像素?cái)?shù)據(jù)。
可以正確的復(fù)制有循環(huán)引用的對(duì)象
依然存在的缺陷是:
Error 以及 Function 對(duì)象是不能被結(jié)構(gòu)化克隆算法復(fù)制的;如果你嘗試這樣子去做,這會(huì)導(dǎo)致拋出 DATA_CLONE_ERR 的異常。
企圖去克隆 DOM 節(jié)點(diǎn)同樣會(huì)拋出 DATA_CLONE_ERROR 異常。
對(duì)象的某些特定參數(shù)也不會(huì)被保留
RegExp 對(duì)象的 lastIndex 字段不會(huì)被保留
屬性描述符,setters 以及 getters(以及其他類似元數(shù)據(jù)的功能)同樣不會(huì)被復(fù)制。例如,如果一個(gè)對(duì)象用屬性描述符標(biāo)記為 read-only,它將會(huì)被復(fù)制為 read-write,因?yàn)檫@是默認(rèn)的情況下。
原形鏈上的屬性也不會(huì)被追蹤以及復(fù)制。
我們先來看看常規(guī)的深拷貝,它跟淺拷貝的區(qū)別在于,當(dāng)我們發(fā)現(xiàn)對(duì)象的屬性是引用類型時(shí),進(jìn)行遞歸遍歷復(fù)制,直到遍歷完所有屬性:
var deepClone = function(currobj){ if(typeof currobj !== "object"){ return currobj; } if(currobj instanceof Array){ var newobj = []; }else{ var newobj = {} } for(var key in currobj){ if(typeof currobj[key] !== "object"){ // 不是引用類型,則復(fù)制值 newobj[key] = currobj[key]; }else{ // 引用類型,則遞歸遍歷復(fù)制對(duì)象 newobj[key] = deepClone(currobj[key]) } } return newobj }
這個(gè)的主要問題就是不處理循環(huán)引用,不處理對(duì)象原型,函數(shù)依然是引用類型。上面描述過的復(fù)雜問題依然存在,可以說是最簡(jiǎn)陋但是日常工作夠用的深拷貝方式。
另外還有一種方式是使用JSON序列化,巧妙但是限制更多:
// 調(diào)用JSON內(nèi)置方法先序列化為字符串再解析還原成對(duì)象 newObj = JSON.parse(JSON.stringify(obj));
JSON是一種表示結(jié)構(gòu)化數(shù)據(jù)的格式,只支持簡(jiǎn)單值、對(duì)象和數(shù)組三種類型,不支持變量、函數(shù)或?qū)ο髮?shí)例。所以我們工作中可以使用它解決常見問題,但也要注意其短板:函數(shù)會(huì)丟失,原型鏈會(huì)丟失,以及上面說到的所有缺陷。
庫實(shí)現(xiàn)上面的兩種方式可以滿足大部分場(chǎng)景的需求,如果有更復(fù)雜的需求,可以自己實(shí)現(xiàn)。現(xiàn)在我們可以看一些框架和庫的解決方案,下面拿經(jīng)典的jQuery和lodash的源碼看下,它們的優(yōu)缺點(diǎn)上面都說過了:
jQuery.extend()// 進(jìn)行深度復(fù)制,如果第一個(gè)參數(shù)為true則深度復(fù)制,如果目標(biāo)對(duì)象不合法,則拋棄并重構(gòu)為{}空對(duì)象,如果只有一個(gè)參數(shù)則功能為擴(kuò)展jQuery對(duì)象 jQuery.extend = jQuery.fn.extend = function() { var options, name, src, copy, copyIsArray, clone, target = arguments[ 0 ] || {}, i = 1, length = arguments.length, deep = false; // Handle a deep copy situation // 第一個(gè)參數(shù)可以為true來確定進(jìn)行深度復(fù)制 if ( typeof target === "boolean" ) { deep = target; // Skip the boolean and the target target = arguments[ i ] || {}; i++; } // Handle case when target is a string or something (possible in deep copy) // 如果目標(biāo)對(duì)象不合法,則強(qiáng)行重構(gòu)為{}空對(duì)象,拋棄原有的 if ( typeof target !== "object" && !jQuery.isFunction( target ) ) { target = {}; } // Extend jQuery itself if only one argument is passed // 如果只有一個(gè)參數(shù),擴(kuò)展jQuery對(duì)象 if ( i === length ) { target = this; i--; } for ( ; i < length; i++ ) { // Only deal with non-null/undefined values // 只處理有值的對(duì)象 if ( ( options = arguments[ i ] ) != null ) { // Extend the base object for ( name in options ) { src = target[ name ]; copy = options[ name ]; // Prevent never-ending loop // 阻止最簡(jiǎn)單形式的循環(huán)引用 // var obj={}, obj2={a:obj}; $.extend(true, obj, obj2); 就會(huì)形成復(fù)制的對(duì)象循環(huán)引用obj if ( target === copy ) { continue; } // 如果為深度復(fù)制,則新建[]和{}空數(shù)組或空對(duì)象,遞歸本函數(shù)進(jìn)行復(fù)制 // Recurse if we"re merging plain objects or arrays if ( deep && copy && ( jQuery.isPlainObject( copy ) || ( copyIsArray = Array.isArray( copy ) ) ) ) { if ( copyIsArray ) { copyIsArray = false; clone = src && Array.isArray( src ) ? src : []; } else { clone = src && jQuery.isPlainObject( src ) ? src : {}; } // Never move original objects, clone them target[ name ] = jQuery.extend( deep, clone, copy ); // Don"t bring in undefined values } else if ( copy !== undefined ) { target[ name ] = copy; } } } } // Return the modified object return target; };lodash _.baseClone()
/** * The base implementation of `_.clone` and `_.cloneDeep` which tracks * traversed objects. * * @private * @param {*} value The value to clone. * @param {boolean} bitmask The bitmask flags. * 1 - Deep clone * 2 - Flatten inherited properties * 4 - Clone symbols * @param {Function} [customizer] The function to customize cloning. * @param {string} [key] The key of `value`. * @param {Object} [object] The parent object of `value`. * @param {Object} [stack] Tracks traversed objects and their clone counterparts. * @returns {*} Returns the cloned value. */ function baseClone(value, bitmask, customizer, key, object, stack) { var result, isDeep = bitmask & CLONE_DEEP_FLAG, isFlat = bitmask & CLONE_FLAT_FLAG, isFull = bitmask & CLONE_SYMBOLS_FLAG; if (customizer) { result = object ? customizer(value, key, object, stack) : customizer(value); } if (result !== undefined) { return result; } if (!isObject(value)) { return value; } var isArr = isArray(value); if (isArr) { result = initCloneArray(value); if (!isDeep) { return copyArray(value, result); } } else { var tag = getTag(value), isFunc = tag == funcTag || tag == genTag; if (isBuffer(value)) { return cloneBuffer(value, isDeep); } if (tag == objectTag || tag == argsTag || (isFunc && !object)) { result = (isFlat || isFunc) ? {} : initCloneObject(value); if (!isDeep) { return isFlat ? copySymbolsIn(value, baseAssignIn(result, value)) : copySymbols(value, baseAssign(result, value)); } } else { if (!cloneableTags[tag]) { return object ? value : {}; } result = initCloneByTag(value, tag, baseClone, isDeep); } } // Check for circular references and return its corresponding clone. stack || (stack = new Stack); var stacked = stack.get(value); if (stacked) { return stacked; } stack.set(value, result); var keysFunc = isFull ? (isFlat ? getAllKeysIn : getAllKeys) : (isFlat ? keysIn : keys); var props = isArr ? undefined : keysFunc(value); arrayEach(props || value, function(subValue, key) { if (props) { key = subValue; subValue = value[key]; } // Recursively populate clone (susceptible to call stack limits). assignValue(result, key, baseClone(subValue, bitmask, customizer, key, value, stack)); }); return result; }參考資料
知乎 JS的深拷貝和淺拷貝: https://www.zhihu.com/questio...
Javascript之深拷貝: https://aepkill.github.io/201...
js對(duì)象克隆之謎:http://b-sirius.me/2017/08/26...
知乎 JS如何完整實(shí)現(xiàn)深度Clone對(duì)象:https://www.zhihu.com/questio...
github lodash源碼:https://github.com/lodash/lod...
MDN 結(jié)構(gòu)化克隆算法:https://developer.mozilla.org...
jQuery v3.2.1 源碼
JavaScript高級(jí)程序設(shè)計(jì) 第4章(變量、作用域和內(nèi)存問題)、第20章(JSON)
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://specialneedsforspecialkids.com/yun/88766.html
摘要:在之前的文章專題之?dāng)?shù)據(jù)類型和類型檢測(cè)中我有講過,中的數(shù)據(jù)類型分為兩種,基本數(shù)據(jù)類型和引用數(shù)據(jù)類型,基本數(shù)據(jù)類型是保存在棧的數(shù)據(jù)結(jié)構(gòu)中的是按值訪問,所以不存在深淺拷貝問題。 前言 在開發(fā)過程中,偶爾會(huì)遇到這種場(chǎng)景,拿到一個(gè)數(shù)據(jù)后,你打算對(duì)它進(jìn)行處理,但是你又希望拷貝一份副本出來,方便數(shù)據(jù)對(duì)比和以后恢復(fù)數(shù)據(jù)。 那么這就涉及到了 JS 中對(duì)數(shù)據(jù)的深淺拷貝問題,所謂深淺拷貝,淺拷貝的意思就是,...
摘要:深拷貝相比于淺拷貝速度較慢并且花銷較大。所以在賦值完成后,在棧內(nèi)存就有兩個(gè)指針指向堆內(nèi)存同一個(gè)數(shù)據(jù)。結(jié)果如下擴(kuò)展運(yùn)算符只能對(duì)一層進(jìn)行深拷貝如果拷貝的層數(shù)超過了一層的話,那么就會(huì)進(jìn)行淺拷貝那么我們可以看到和展開原算符對(duì)于深淺拷貝的結(jié)果是一樣。 JS中數(shù)據(jù)類型 基本數(shù)據(jù)類型: undefined、null、Boolean、Number、String和Symbol(ES6) 引用數(shù)據(jù)類型:...
摘要:正文討論深淺拷貝,首先要從的基本數(shù)據(jù)類型說起根據(jù)中的變量類型傳遞方式,分為值類型和引用類型,值類型變量包括。當(dāng)你拷貝的對(duì)象有多級(jí)的時(shí)候,就是深拷貝。數(shù)據(jù)不存在則對(duì)其拷貝。 前言: 本文主要閱讀對(duì)象:對(duì)深淺拷貝印象模糊對(duì)初級(jí)前端,想對(duì)js深淺拷貝聊一聊的中級(jí)前端。 如果是對(duì)這些有完整對(duì)認(rèn)知體系和解決方法的大佬,可以選擇略過。 正文: 討論深淺拷貝,首先要從js的基本數(shù)據(jù)類型說起: 根據(jù) J...
摘要:深復(fù)制實(shí)現(xiàn)代碼如下第一種方法通過遞歸解析解決第二種方法通過解析解決作者六師兄鏈接原生深拷貝的實(shí)現(xiàn)處理未輸入新對(duì)象的情況通過方法構(gòu)造新的對(duì)象 深淺拷貝針對(duì)的是 對(duì)象類型,如果是字符串的數(shù)組用[...arr],還是不會(huì)影響 要區(qū)分針對(duì)數(shù)組的深淺拷貝(默認(rèn)情況為里面沒有對(duì)象的數(shù)組),與針對(duì)對(duì)象的深淺拷貝 JavaScript數(shù)組深拷貝和淺拷貝的兩種方法 let a1 = [1, 2]; ...
摘要:基本數(shù)據(jù)類型的復(fù)制很簡(jiǎn)單,就是賦值操作,所以深淺拷貝也是針對(duì),這類引用類型數(shù)據(jù)。它會(huì)拋棄對(duì)象的。另外,查資料過程中還看到這么一個(gè)詞結(jié)構(gòu)化克隆算法還有這一篇資料也有參考,也寫得比較詳細(xì)了的深淺拷貝 基本數(shù)據(jù)類型的復(fù)制很簡(jiǎn)單,就是賦值操作,所以深淺拷貝也是針對(duì)Object,Array這類引用類型數(shù)據(jù)。 淺拷貝對(duì)于字符串來說,是值的復(fù)制,而對(duì)于對(duì)象來說則是對(duì)對(duì)象地址的復(fù)制;而深拷貝的話,它不...
摘要:圖數(shù)據(jù)類型圖引用類型深淺拷貝問題不知道什么是深拷貝和淺拷貝的請(qǐng)先去并在調(diào)試臺(tái)自己操作一下,這篇文章只會(huì)說明為何中會(huì)有這種問題。所以有的時(shí)候我們?yōu)榱吮苊鉁\拷貝,會(huì)用一些方式實(shí)現(xiàn)深拷貝。 首先要了解的js基礎(chǔ) 基本數(shù)據(jù)類型:Object、undefined、null、Boolean、Number、String、Symbol (ES6新加) Object包括: Array 、Date 、R...
閱讀 3205·2021-11-08 13:21
閱讀 1195·2021-08-12 13:28
閱讀 1406·2019-08-30 14:23
閱讀 1924·2019-08-30 11:09
閱讀 840·2019-08-29 13:22
閱讀 2684·2019-08-29 13:12
閱讀 2548·2019-08-26 17:04
閱讀 2250·2019-08-26 13:22