JavaScript的深拷貝和淺拷貝
來(lái)源:https://segmentfault.com/a/1190000017469386
一直想梳理下工作中經(jīng)常會(huì)用到的深拷貝的內(nèi)容,然而遍覽了許多的文章,卻發(fā)現(xiàn)對(duì)深拷貝并沒(méi)有一個(gè)通用的完美實(shí)現(xiàn)方式。因?yàn)閷?duì)深拷貝的定義不同,實(shí)現(xiàn)時(shí)的edge case過(guò)多,在深拷貝的時(shí)候會(huì)出現(xiàn)循環(huán)引用等問(wèn)題,導(dǎo)致JS內(nèi)部并沒(méi)有實(shí)現(xiàn)深拷貝,但是我們可以來(lái)探究一下深拷貝到底有多復(fù)雜,各種實(shí)現(xiàn)方式的優(yōu)缺點(diǎn),同時(shí)參考下常用庫(kù)對(duì)其的實(shí)現(xiàn)。
引用類型
之所以會(huì)出現(xiàn)深淺拷貝的問(wè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'
淺拷貝
如果我們要復(fù)制對(duì)象的所有屬性都不是引用類型時(shí),就可以使用淺拷貝,實(shí)現(xiàn)方式就是遍歷并復(fù)制,最后返回新的對(duì)象。
function shallowCopy(obj) {
var copy = {};
// 只復(fù)制可遍歷的屬性
for (key in obj) {
// 只復(fù)制本身?yè)碛械膶傩?
if (obj.hasOwnProperty(key)) {
copy[key] = obj[key];
}
}
return copy;
}
如上面所說(shuō),我們使用淺拷貝會(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);
深拷貝
如果我們需要復(fù)制一個(gè)擁有所有屬性和方法的新對(duì)象,就要用到深拷貝,JS并沒(méi)有內(nèi)置深拷貝方法,主要是因?yàn)椋?/p>
- 深拷貝怎么定義?我們?cè)趺刺幚碓停吭趺磪^(qū)分可拷貝的對(duì)象?原生DOM/BOM對(duì)象怎么拷貝?函數(shù)是新建還是引用?這些edge case太多導(dǎo)致我們無(wú)法統(tǒng)一概念,造出大家都滿意的深拷貝方法來(lái)。
- 內(nèi)部循環(huán)引用怎么處理,是不是保存每個(gè)遍歷過(guò)的對(duì)象列表,每次進(jìn)行對(duì)比,然后再造一個(gè)循環(huán)引用來(lái)?這樣帶來(lái)的性能消耗可以接受嗎。
解釋一些常見(jiàn)的問(wè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ì)象,那么無(wú)論是循環(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
來(lái)查詢是否是本身元素。在原型鏈和可遍歷屬性和自身屬性之間存在交集,但都不相等,我們應(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;
}
雖然邊界條件很多,但是不同的框架和庫(kù)都對(duì)該方法進(jìn)行了實(shí)現(xiàn),只不過(guò)定義不同,實(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ù)制出來(lái)相同的像素?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ù)制。
我們先來(lái)看看常規(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è)的主要問(wèn)題就是不處理循環(huán)引用,不處理對(duì)象原型,函數(shù)依然是引用類型。上面描述過(guò)的復(fù)雜問(wèn)題依然存在,可以說(shuō)是最簡(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í)例。所以我們工作中可以使用它解決常見(jiàn)問(wèn)題,但也要注意其短板:函數(shù)會(huì)丟失,原型鏈會(huì)丟失,以及上面說(shuō)到的所有缺陷。
庫(kù)實(shí)現(xiàn)
上面的兩種方式可以滿足大部分場(chǎng)景的需求,如果有更復(fù)雜的需求,可以自己實(shí)現(xiàn)。現(xiàn)在我們可以看一些框架和庫(kù)的解決方案,下面拿經(jīng)典的jQuery和lodash的源碼看下,它們的優(yōu)缺點(diǎn)上面都說(shuō)過(guò)了:
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來(lái)確定進(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)存問(wèn)題)、第20章(JSON)