拷貝,在js中,分為淺拷貝和深拷貝。這兩者是如何區分的呢?又是如何實現的呢?
深淺拷貝的區分
首先說下,在js中,分為基礎數據類型和復雜數據類型,
基礎數據類型:Undefined、Null、Boolean、Number、String、Symbol
復雜數據類型:Object、Array、Function、Date等
基礎數據類型值,存儲在棧(stack)中,拷貝的話,會重新在棧中開辟一個相同的空間存儲數據。而復雜數據類型,值存儲在堆(heap)中,棧中存儲對值的引用地址。深淺拷貝,只針對復雜數據類型來說的。
淺拷貝ShallowCopy,是一個對象的逐位副本。創建一個新對象,該對象具有原始對象中的精確副本。如果對象的任何字段是對其他對象的引用,則只復制引用地址,即只復制內存地址,而不復制對象本身,新舊對象還是共享同一塊堆內存。改變其中一個對象,另一個也會受影響。如果有修改,會失去原始數據。
深拷貝DeepCopy,復制出一個全新的對象實例,新對象跟原對象不共享內存,兩者操作互不影響。
簡單點區分,
淺拷貝拷貝引用;
深拷貝拷貝實例。
ShallowCopy淺拷貝的實現方式
1. 賦值
先來說說,簡單的賦值情況,
var o1 = { a : 1, b : 2 }
var o2 = o1
console.log(o2 === o1) // true
o1.a = 2
console.log(o1) // {a: 2, b: 2}
console.log(o2) // {a: 2, b: 2}
賦值,這里是對對象地址的引用,改變一個對象的值,拷貝的另一個對象的值也跟着變化,所以這是淺拷貝。
2. Array.concat()
concat方法用於合並兩個或多個數組。該方法不會更改現有的數組,而僅僅會返回被連接數組的一個副本。
var o1 = [1, [2], 3]
var o2 = o1.concat([]) // 這里會返回一個o1對象的淺拷貝對象
console.log(o2) // [1, [2], 3]
console.log(o1 === o2) // false
o2數組就是一個新數組。如果改變o1數組對象,會不會影響o2數組對象呢?
o1[0] = 11
console.log(o1) // [11, [2], 3]
console.log(o2) // [1, [2], 3]
以上這種情況,沒有改變o2數組值。這是因為,o2中第一個元素和o1中的第一個元素,不是同一個內存地址。
o1[1][0] = 22
console.log(o1) // [11, [22], 3]
console.log(o2) // [1, [22], 3]
而修改o1變量中的引用的值,o2數組值也跟隨着變化。這說明,o2中第二個元素和o1中的第二個元素引用相同的內存地址。
根據以上的說明,可以得出結論,如果數組是一維數組,則可以算是深拷貝。如果是多維數組,則是淺拷貝。
3. Array.slice()
slice方法可從已有的數組中返回選定的元素。
var o1 = [1, [2], 3]
var o2 = o1.slice(0)
console.log(o1) // [1, [2], 3]
console.log(o2) // [1, [2], 3]
該方法不會修改數組,而是返回一個子數組。
o1[0] = 11
console.log(o1) // [11, [2], 3]
console.log(o2) // [1, [2], 3]
從結果看出,只修改了o1的值,o2的值沒有修改。
o1[1][0] = 22
console.log(o1) // [11, [22], 3]
console.log(o2) // [1, [22], 3]
從結果看出,o1、o2兩個變量的值,都發生了變化。說明,兩者引用指向同一個內存地址。
以上,說明是淺拷貝。
4. Object.assign()
Object.assign()方法用於將所有可枚舉的自有屬性的值從一個或多個源對象復制到目標對象。 它將返回目標對象
var o1 = { a : 1, b : { c : 2, d : 3} }
var o2 = Object.assign({}, o1)
console.log(o1) // { a : 1, b : { c : 2, d : 3} }
console.log(o2) // { a : 1, b : { c : 2, d : 3} }
console.log(o2 === o1) // false 說明實現了淺拷貝
o1.a = 11
console.log(o2) // { a : 1, b : { c : 2, d : 3} } o1和o2內部包含的基本類型值,拷貝的是其實例,不會相互影響
o1.b.c = 22
console.log(o1) // { a : 11, b : { c : 22, d : 3} }
console.log(o2) // { a : 1, b : { c : 22, d : 3} } o1和o2內部包含的引用類型值,拷貝的是其引用,會相互影響
5. 使用jQuery中的extend函數
// Shallow copy
jQuery.extend({},OriginalObject)
// Deep copy
jQuery.extend(true, {},OriginalObject)
jQuery.extend( [deep ], target, object1 [, objectN ] )
,其中deep為Boolean類型,如果是true,則進行深拷貝。
var $ = require('jquery')
var o1 = { a : 1, b : { c : 2 } }
var o2 = $.extend({}, o1)
console.log(o1.b === o2.b) // true
console.log(o1.a === o1.a) // false
6. lodash中的 _.clone()
利用結構化拷貝算法。支持拷貝arrays,array buffers,booleans, data objects, maps,
numbers, Object
objects, regexes, sets, strings, symbols, and typed
arrays. arguments
對象的可枚舉屬性被拷貝為普通對象。
為不可拷貝的值(如錯誤對象、函數、DOM節點和弱映射)返回一個空對象。
淺拷貝: _.clone()
深拷貝:_.cloneDeep()
var objects = [{ 'a': 1 }, { 'b': 2 }];
var shallow = _.clone(objects);
console.log(shallow[0] === objects[0]); // true
objects[0].a = 11
console.log(shallow[0]) // { a : 11}
DeepCopy深拷貝的實現方式
1. 手動復制
要實現拷貝出來的副本,不受原本影響,那么可以這么實現
var o1 = { a : 1, b : 2 }
var o2 = { a : o1.a, b : o1.b }
console.log(o2 === o1) // false
o1.a = 2
console.log(o1) // {a: 2, b: 2}
console.log(o2) // {a: 1, b: 2}
將每個引用對象都通過復制值來實現深拷貝。
2. JSON.parse(JSON.stringify(object_array))
-
JSON.stringify(): 把對象轉換為字符串
-
JSON.parse():把字符串轉換為對象
var o1 = { a : 1, b : { c : 2} }
var o2 = JSON.parse(JSON.stringify(o1))
console.log(o1 === o2) // false
console.log(o1.b === o2.b) // false
o1.b.c = 22
o1.a = 11
console.log(o1) // { a : 11, b : { c : 22} }
console.log(o2) // { a : 1, b : { c : 2} }
這種方式,只針對可以轉換為JSON對象的類型,比如Array,Object。如果遇到Function就不適用了。
3. 再次遇見jQuery.extend()方法
jQuery.extend( [deep ], target, object1 [, objectN ] )
,其中deep為Boolean類型,如果是true,則進行深拷貝。
// jQuery.extend()源碼
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 處理深拷貝
if ( typeof target === "boolean" ) {
deep = target;
// Skip the boolean and the target
// 跳過布爾和目標,重新賦值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
// 只處理non-null/undefined的值
if ( ( options = arguments[ i ] ) != null ) {
// Extend the base object
// 展開基本/源對象
for ( name in options ) {
src = target[ name ];
copy = options[ name ];
// Prevent never-ending loop
// 防止無限循環
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;
};
4. lodash中的_.cloneDeep()
利用第三方庫lodash,它的深拷貝函數cloneDeep(),這個函數還是比較靠譜的,大多數需求都能滿足。
var o1 = { a : 1, b : { c : 2} }
var o2 = _.cloneDeep(o1)
console.log(o1 === o2) // false
o1.a = 11
o1.b.c = 22
console.log(o1) // { a : 11, b : { c : 22} }
console.log(o2) // { a : 1, b : { c : 2} }
5. 自己實現深拷貝
針對Array和Object兩種復雜類型,自己實現深拷貝。自己實現的深拷貝,對比jquery的。沒有考慮undefined和null值。
// 檢測數據類型的函數
function typeString(obj) {
var cons = Object.prototype.toString.call(obj).slice(8, -1)
return (cons === 'Array' || cons === 'Object')
}
// 實現深度拷貝 Array/Object
function deepClone(oldObj) {
if(typeString(oldObj)) {
var newObj = oldObj.constructor()
for(let i in oldObj) {
if (oldObj.hasOwnProperty(i)) {
newObj[i] = typeString(oldObj[i]) ? deepClone(oldObj[i]) : oldObj[i]
}
}
return newObj;
} else {
return oldObj
}
}
// 測試
var o1 = [1, 2, [3, 4]]
var o2 = deepClone(o1)
console.log(o1 === o2) // false
o1[2][0] = 2018
console.log(o2) // [1, 2, [3, 4]]
console.log(o1) // [1, 2, [2018, 4]]
深淺拷貝總結
拷貝之后,是否會相互影響,是個重要的指標。以上討論的深淺拷貝,針對的范圍比較小,大部分只考慮了Object和Array類型,但是能在大多數場合使用。
Function、Data、RegExp等沒有考慮。如果需要考慮這些,可以針對特定的情況,來具體實現,比如lodash的_.clone,就是使用的結構化拷貝算法,針對不同情況來拷貝的。