一直想梳理下工做中常常會用到的深拷貝的內容,然而遍覽了許多的文章,卻發現對深拷貝並無一個通用的完美實現方式。由於對深拷貝的定義不一樣,實現時的edge case
過多,在深拷貝的時候會出現循環引用等問題,致使JS內部並無實現深拷貝,可是咱們能夠來探究一下深拷貝到底有多複雜,各類實現方式的優缺點,同時參考下經常使用庫對其的實現。git
之因此會出現深淺拷貝的問題,實質上是因爲JS對基本類型和引用類型的處理不一樣。基本類型指的是簡單的數據段,而引用類型指的是一個對象,而JS不容許咱們直接操做內存中的地址,也就是不能操做對象的內存空間,因此,咱們對對象的操做都只是在操做它的引用而已。github
在複製時也是同樣,若是咱們複製一個基本類型的值時,會建立一個新值,並把它保存在新的變量的位置上。而若是咱們複製一個引用類型時,一樣會把變量中的值複製一份放到新的變量空間裏,但此時複製的東西並非對象自己,而是指向該對象的指針。因此咱們複製引用類型後,兩個變量其實指向同一個對象,改變其中一個對象,會影響到另一個。算法
var num = 10; var obj = { name: 'Nicholas' } var num2 = num; var obj2 = obj; obj.name = 'Lee'; obj2.name; // 'Lee'
能夠看到咱們的obj和obj2都保存了一個指向該對象的指針,全部的操做都是對該引用的操做,因此對對象的修改會影響其餘的複製對象。數組
若是咱們要複製對象的全部屬性都不是引用類型時,就可使用淺拷貝,實現方式就是遍歷並複製,最後返回新的對象。框架
function shallowCopy(obj) { var copy = {}; // 只複製可遍歷的屬性 for (key in obj) { // 只複製自己擁有的屬性 if (obj.hasOwnProperty(key)) { copy[key] = obj[key]; } } return copy; }
如上面所說,咱們使用淺拷貝會複製全部引用對象的指針,而不是具體的值,因此使用時必定要明確本身的需求,同時,淺拷貝的實現也是最簡單的。ide
JS內部實現了淺拷貝,如Object.assign()
,其中第一個參數是咱們最終複製的目標對象,後面的全部參數是咱們的即將複製的源對象,支持對象或數組,通常調用的方式爲函數
var newObj = Object.assign({}, originObj);
這樣咱們就獲得了一個新的淺拷貝對象。另外[].slice()
方法能夠視爲數組對象的淺拷貝。oop
若是咱們須要複製一個擁有全部屬性和方法的新對象,就要用到深拷貝,JS並無內置深拷貝方法,主要是由於:性能
解釋一些常見的問題概念,防止有些同窗不明白咱們在講什麼。好比循環引用:ui
var obj = {}; obj.b = obj;
這樣當咱們深拷貝obj對象時,就會循環的遍歷b屬性,直到棧溢出。
咱們的解決方案爲創建一個集合[]
,每次遍歷對象進行比較,若是[]
中已存在,則證實出現了循環引用或者相同引用,咱們直接返回該對象已複製的引用便可:
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('存在循環引用或屬性引用了相同對象'); // 若是已存在,證實引用了相同對象,那麼不管是循環引用仍是重複引用,咱們返回引用就能夠了 copy[i] = hasObj[index]; } else { copy[i] = referCopy(obj[i]); } } else { copy[i] = obj[i]; } } return copy; }
處理原型和區分可拷貝的對象:咱們通常使用function.prototype
指代原型,使用obj.__proto__
指代原型鏈,使用enumerable
屬性表示是否能夠被for ... in
等遍歷,使用hasOwnProperty
來查詢是不是自己元素。在原型鏈和可遍歷屬性和自身屬性之間存在交集,但都不相等,咱們應該如何判斷哪些屬性應該被複制呢?
函數的處理:函數擁有一些內在屬性,但咱們通常不修改這些屬性,因此函數通常直接引用其地址便可。可是擁有一些存取器屬性的函數咱們怎麼處理?是複製值仍是複製存取描述符?
var obj = { age: 10, get age() { return this.age; }, set age(age) { this.age = age; } }; var obj2 = $.extend(true, {}, obj); obj2; // {age: 10}
這個是咱們想要的結果嗎?大部分場景下不是吧,好比我要複製一個已有的Vue對象。固然咱們也有解決方案:
function copy(obj) { var copy = {}; for (var i in obj) { let desc = Object.getOwnPropertyDescriptor(obj, i); // 檢測是否爲存取描述符 if (desc.set || desc.get) { Object.defineProperty(copy, i, { get: desc.get, set: desc.set, configuarable: desc.configuarable, enumerable: true }); // 不然爲數據描述符,則複用下面的深拷貝方法,此處簡寫 } else { copy[i] = obj[i]; } } return copy; }
雖然邊界條件不少,可是不一樣的框架和庫都對該方法進行了實現,只不過定義不一樣,實現方式也不一樣,如jQuery.extend()
只複製可枚舉的屬性,不繼承原型鏈,函數複製引用,內部循環引用不處理。而lodash實現的就更爲優秀,它實現了結構化克隆算法
。
該算法的優勢是:
依然存在的缺陷是:
對象的某些特定參數也不會被保留
咱們先來看看常規的深拷貝,它跟淺拷貝的區別在於,當咱們發現對象的屬性是引用類型時,進行遞歸遍歷複製,直到遍歷完全部屬性:
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'){ // 不是引用類型,則複製值 newobj[key] = currobj[key]; }else{ // 引用類型,則遞歸遍歷複製對象 newobj[key] = deepClone(currobj[key]) } } return newobj }
這個的主要問題就是不處理循環引用,不處理對象原型,函數依然是引用類型。上面描述過的複雜問題依然存在,能夠說是最簡陋可是平常工做夠用的深拷貝方式。
另外還有一種方式是使用JSON序列化,巧妙可是限制更多:
// 調用JSON內置方法先序列化爲字符串再解析還原成對象 newObj = JSON.parse(JSON.stringify(obj));
JSON是一種表示結構化數據的格式,只支持簡單值、對象和數組三種類型,不支持變量、函數或對象實例。因此咱們工做中可使用它解決常見問題,但也要注意其短板:函數會丟失,原型鏈會丟失,以及上面說到的全部缺陷。
上面的兩種方式能夠知足大部分場景的需求,若是有更復雜的需求,能夠本身實現。如今咱們能夠看一些框架和庫的解決方案,下面拿經典的jQuery和lodash的源碼看下,它們的優缺點上面都說過了:
// 進行深度複製,若是第一個參數爲true則深度複製,若是目標對象不合法,則拋棄並重構爲{}空對象,若是隻有一個參數則功能爲擴展jQuery對象 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 // 第一個參數能夠爲true來肯定進行深度複製 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) // 若是目標對象不合法,則強行重構爲{}空對象,拋棄原有的 if ( typeof target !== "object" && !jQuery.isFunction( target ) ) { target = {}; } // Extend jQuery itself if only one argument is passed // 若是隻有一個參數,擴展jQuery對象 if ( i === length ) { target = this; i--; } for ( ; i < length; i++ ) { // Only deal with non-null/undefined values // 只處理有值的對象 if ( ( options = arguments[ i ] ) != null ) { // Extend the base object for ( name in options ) { src = target[ name ]; copy = options[ name ]; // Prevent never-ending loop // 阻止最簡單形式的循環引用 // var obj={}, obj2={a:obj}; $.extend(true, obj, obj2); 就會造成複製的對象循環引用obj if ( target === copy ) { continue; } // 若是爲深度複製,則新建[]和{}空數組或空對象,遞歸本函數進行復制 // 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; };
/** * 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; }