在JavaScript中對象的淺拷貝和深拷貝有如下差別:
淺拷貝:僅僅複制對象的引用,而不是對象本身。
深拷貝:複制對象所引用的全部對象。
我在平常練習時,常使用的2種淺拷貝和三種深拷貝的方法。
淺拷貝:
1.自定義實作
function simpleClone(obj) {
var simpleCloneObj = {};
for (var i in obj) {
simpleCloneObj[i] = obj[i];
}
return simpleCloneObj;
}
2.使用Object.assign() 方法可以把任意多個的源對象自身的可枚舉屬性拷貝給目标對象,然後傳回目标對象。但是 Object.assign() 進行的是淺拷貝,拷貝的是對象的屬性的引用,而不是對象本身。
var simpleClone= Object.assign({}, obj);
深拷貝:
1.使用 JSON.parse() 方法
function deepClone(obj) {
var deepCloneObj = {};
try {
deepCloneObj = JSON.parse(JSON.stringify(obj));
}
catch (e) {
}
return deepCloneObj;
}
這種方法雖然簡單,但是有如下的問題,它會抛棄對象的constructor,也就是深拷貝之後,不管這個對象原來的構造函數是什麼,在深拷貝之後都會變成Object。
這種方法能正确處理的對象隻有 Number, String, Boolean, Array, 扁平對象(自己百度一下),即那些能夠被 json 直接表示的資料結構。RegExp對象是無法通過這種方式深拷貝。
-
遞歸拷貝
第二個參數可以用來實作追加。
function deepClone(initalObj, finalObj) {
var deepCloneObj = finalObj || {};
for (var i in initalObj) {
var prop = initalObj[i];
// 避免互相引用對象導緻死循環,如initalObj.a = initalObj的情況
if (prop === deepCloneObj) {
continue;
}
if (typeof prop === 'object') {
deepCloneObj[i] = (prop.constructor === Array) ? [] : {};
arguments.callee(prop, deepCloneObj[i]);
} else {
deepCloneObj[i] = prop;
}
}
return deepCloneObj;
}
3.使用Object.create()方法
function deepClone(initalObj, finalObj) {
var deepCloneObj = finalObj || {};
for (var i in initalObj) {
var prop = initalObj[i];
// 避免互相引用對象導緻死循環,如initalObj.a = initalObj的情況
if (prop === deepCloneObj) {
continue;
}
if (typeof prop === 'object') {
deepCloneObj[i] = (prop.constructor === Array) ? [] : Object.create(prop);
} else {
deepCloneObj[i] = prop;
}
}
return deepCloneObj;
}
當直接使用下面的方法也可以達到深拷貝的效果。
var deepCloneObj= Object.create(oldObj);
那麼現在來測試一下淺拷貝的方法。
var cloneObj = simpleClone(obj);
console.log(cloneObj.name);
console.log(cloneObj.val);
cloneObj.name = "simpleCloneTest2";
cloneObj.val = [, , ];
console.log(cloneObj.val);
console.log(obj.name);
console.log(obj.val);
執行結果如下:
simpleCloneTest
[0, 1, 2]
simpleCloneTest2
[3, 4, 5]
simpleCloneTest
[0, 1, 2]
我們發現在cloneObj 更改name和val時,obj的值并沒有更改。這是為什麼呢?我們稍後說明。
我們再來另一種測試。
var obj = {
name: "simpleCloneTest",
val: [, , ]
};
var cloneObj = simpleClone(obj);
console.log(cloneObj.name);
console.log(cloneObj.val);
//cloneObj.name = "simpleCloneTest2";
//cloneObj.val = [, , ];
//console.log(cloneObj.name);
//console.log(cloneObj.val);
//console.log(obj.name);
//console.log(obj.val);
cloneObj.name = "simpleCloneTest3";
cloneObj.val[] = ;
console.log(cloneObj.name);
console.log(cloneObj.val[]);
console.log(obj.name);
console.log(obj.val[]);
輸出結果如下:
simpleCloneTest
[0, 1, 2]
simpleCloneTest3
3
simpleCloneTest
3
一下内容來自于:https://yq.aliyun.com/articles/35053
這時我們發現clone和原來的obj的值都更改了。這讓我百思不得其解,最終找到一篇很有說服力的文章,進行複制過來(避免文章被删帖)。
outline:
為什麼要說JS中深拷貝與淺拷貝
JS對類型的分類
immutable與mutable
簡單類型檢測
淺拷貝VS深拷貝
為什麼要說JS中深拷貝與淺拷貝
近來在研讀underscore的源碼,發現其中一小段代碼
_.mixin = function(obj) {
_.each(_.functions(obj), function(name) {
var func = _[name] = obj[name];
_.prototype[name] = function() {
var args = [this._wrapped];
push.apply(args, arguments);
return result(this, func.apply(_, args));
};
});
};
....
_.mixin(_);
這段代碼就是要把我們在上綁的很多方法*淺拷貝*一份到.prototype.這裡的淺拷貝引發一些思考.那麼什麼是淺拷貝、什麼是深拷貝?在了解深、淺拷貝之前,我們需要了解下JS中對類型的分類,因為對于不同的類型,我們選擇拷貝的方式也是不一樣的.
**
JS對類型的分類
**
stackoverflow有人提了一個問題:
Stoyan Stefanov in his excellent book ‘Object-Oriented JavaScript’ says:
Any value that doesn’t belong to one of the five primitive types listed above is an object.
Stoyan Stefanov說的這句話,在JS中要麼就是primitive類型,要們就是object類型.
*Primitive
A primitive (primitive value, primitive data type) is data that is not an object and has no methods. In >JavaScript, there are 6 primitive data types: string, number, boolean, null, undefined, symbol (new in ECMAScript 2015).
Most of the time, a primitive value is represented directly at the lowest level of the language implementation.
All primitives are immutable (cannot be changed).*
MDN上指出了JS中的primitive類型一共就是string number boolean null undefined symbol(ES2015)6中類型,其餘的都是object類型.
MDN還說了primitive類型not an object以及has no methods.但是我們平時的使用都是這樣的var str = “hello world”;console.log(str.charAt(0)).這段代碼中明顯str是primitive的變量,按照MDN的說法,str變量應該是not an object并且has no methods的,這裡我們明顯調用了str.charAt方法.是我們錯了還是MDN錯了!!!!那我們再測試下str是不是一個object.Object.prototype.toString.call(str)這段代碼執行的結果居然是[object String].就是說str不僅是object同時還has methods.但是str确實是primitive類型的.
在MDN給出primitive type定義的同時,還給出了Primitive wrapper objects的定義
*Except for null and undefined, all primitive values have object equivalents that wrap around the >primitive values:
String for the string primitive.
Number for the number primitive.
Boolean for the Boolean primitive.
Symbol for the Symbol primitive.
The wrapper’s valueOf() method returns the primitive value.*
也就是說對于這些primitive的類型,确實不是object,并且也沒有methods.執行str.charAt的時候是把string(primitive)類型轉成了String(object)類型.ES5規範中這樣解釋:
這裡雖然對于一些内部方法的調用我們并不清楚,但是基本也明确當我們在調用str.charAt的時候,JS執行引擎把str變成了String對象,可以執行String上的方法.了解了JS中的類型分類,我們在說一說JS中mutable和immutable.
immutable與mutable
在上一段我們講了JS中的類型分類,總體來說就兩類就是object和primitive,判斷依據就是隻有string、number、boolean、null、undefined、symbol(ES2015)才是primitive的,其餘均為object的.在我們引用MDN的一段話中,還提到了All primitives are immutable (cannot be changed).那麼這句話是什麼意思.所有的primitive都是immutable(不可變的).這句話可能大家看完很不了解.var a = 1;a = 2; a= “hello world”;,這裡a就是primitive的類型,不是可以修改麼,那MDN的這句All primitives are immutable是什麼意思呢.
MDN的這句話其實是沒錯誤的.碰到這種問題,查記憶體位址是最好的辦法,可惜查記憶體位址難度太大,在chrome和nodejs上我都嘗試了,都沒有找個有一個比較直覺的方式去看記憶體位址,如果有讀者了解如何看記憶體可以和我聯系.這裡我們借用JS中的原型鍊來做一個小實驗,也可以間接達到檢視記憶體位址的目的.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>immutalbe&mutable</title>
</head>
<body>
<script>
/*
* 這裡的關鍵還是這個立即執行函數.立即執行函數與function的定義夾出了一個不可回收的區域,也就是var id = 0;
* (不明白的可以參考我的http://warjiang.github.io/devcat/2016/04/16/JSLecture/關于閉包的文章)
* 然後我們定義一個函數generateId,負責給id自增.
* 下面是關鍵
* 我們在Object.prototype上擴充了一個id的方法.
* 由于JS中的原型鍊,給Object.prototype擴充方法等于說給所有的對象都擴充了id的這個方法.
* 當某個對象調用id的方法會自動順着原型鍊回溯到Object.prototype上的id方法.
* 調用這個方法的時候,方法中的this指向調用這個方法的對象.也就是給這個調用者擴充了id這個方法.
* 由于id在立即執行函數内,generateId和Object.prototype.id外,
* 是以id在執行過程中并不會被釋放,而是從0開始不斷加1
* 參考自http://stackoverflow.com/questions/2020670/javascript-object-id
*/
(function() {
var id = ;
function generateId() { return id++; console.log(id)};
Object.prototype.id = function() {
var newId = generateId();
this.id = function() { return newId; };
return newId;
};
})();
var a = ;
console.log(a.id());//0
a = ;
console.log(a.id());//1
a = "hello world";
console.log(a.id());//2
</script>
</body>
</html>
這裡id從0變為1、2就是說,我們的a指派的過程并不是給a指向的記憶體指派,而是說a重新指向了一個新的值.基于此,MDN所謂的primitive是immutable的,說的是primitive類型的value是immutable的,而variable是mutable的.是以說,對于primitive類型的變量,為其指派,本質上就是讓變量指向新的記憶體.
那麼對于object類型的變量呢.我們也來做一個實驗:
(function() {
var id = ;
function generateId() { return id++; console.log(id)};
Object.prototype.id = function() {
var newId = generateId();
this.id = function() { return newId; };
return newId;
};
})();
var o1 = {name:'o1'};
console.log(o1.id());//0
var o2 = o1;
console.log(o2.id());//0
o2.name = "o2";
console.log('o1',o1);//o1 Object {name: "o2"}
console.log('o2',o2);//o2 Object {name: "o2"}
o2 = {name:'xx'}
console.log(o2.id())//1
console.log('o1',o1);//o1 Object {name: "o2"}
console.log('o2',o2);//o2 Object {name: "xx"}
從這個例子我們可以看出,對于Object類型的變量,直接指派過程等于說讓變量指向右值記憶體位址.如var o2 = o1,o2就是指向o1指向的記憶體空間.但是當我們修改對象的屬性的時候,就會修改原來記憶體中對象的屬性值.如果o2.name = “o2”會令o1.name ==”o2”.這裡就會引發一個深拷貝、淺拷貝的問題.比如這裡的o2 = o1就是一次淺拷貝.淺拷貝的時候,由于指向的記憶體位址是一樣的,如果直接給對象指派是不存在任何問題的比如var o2 = o1;o2 = {name:’xx’}此時o1.id()傳回0,o2.id()傳回1.但是如果修改對象上的屬性時,就會觸發對象指向的記憶體中的對象的屬性修改.
我們在來看另外一個例子:
(function() {
var id = ;
function generateId() { return id++; console.log(id)};
Object.prototype.id = function() {
var newId = generateId();
this.id = function() { return newId; };
return newId;
};
})();
var o1 = {name:'o1'};
console.log(o1.id());//0
var o2 = {}
o2.name = o1.name;
console.log(o2.id());//1
o2.name = "o2";
console.log('o1',o1);//o1 Object {name: "o1"}
console.log('o2',o2);//o2 Object {name: "o2"}
在這個例子中我們對于o2的指派沒有采用o2 = o1;而是采用了o2={},o2.name = o1.name.那麼這樣夠不夠.結合我麼之前說的immutable和mutable,由于name對應的值是string類型的,是immutable的,是以這裡我們拷貝到name是完全夠的,是屬于深拷貝.
看到這裡,相信大家後面我們要做的深、淺拷貝可能有一定的想法了.淺拷貝就是直接指派,或者說不完全的指派(對于對象而言,後面我們會舉例),淺拷貝對于primitive類型的或者說不會直接修改屬性的對象而言比如Function是無害的,但是對于淺拷貝{k1:v1}或者說是[v1,v2]的對象,會出現嚴重的問題,即由于指向同一個記憶體對象,修改屬性等于修改了所有指向該記憶體對象的屬性.
那麼下面我們就需要做類型檢測,對于做深拷貝需要檢測的情況很簡單,如果檢測出來是淺拷貝有害的,我們就做深拷貝,否則直接淺拷貝.
簡單類型檢測
這裡我們隻需要做Object和Array的類型檢測,對于Function、Date等類型的我們都不是很需要.類型檢測我們采用Object.prototype.toString方法
var isType = function(type){
return function(obj){
return Object.prototype.toString.call(obj) === '[object '+ type +']';
}
}
var is = {
isArray : isType('Array'),
isObject : isType('Object'),
}
有了類型檢測函數,下面我們就可以開心的做深拷貝了.
淺拷貝VS深拷貝
淺拷貝我們之前也說了,這裡直接舉個例子,說明其危害.
(function() {
var id =;
function generateId() { return id++; console.log(id)};
Object.prototype.id = function() {
var newId = generateId();
this.id = function() { return newId; }; return newId; };
})();
var o1 = {
number,
string: "I am a string",
object: { test1: "Old value" },
arr: [ "a string", { test2: "Try changing me" } ]
};
var extend = function(result, source) {
for (var key in source)
result[key] = source[key];
return result;
}
var o2 = extend({},o1);
console.log('o1',o1.number.id());//0
console.log('o1',o1.string.id());//1
console.log('o1',o1.object.id());//2
console.log('o1',o1.arr.id());//3
console.log('o2',o2.number.id());//4
console.log('o2',o2.string.id());//5
console.log('o2',o2.object.id());//2
console.log('o2',o2.arr.id());//3
從id的值上看,o2和o1的内部屬性值,number、string是采用的兩個副本,但是object和arr确實采用的同一個副本.這種情況下如果我們修改o2.object = {name:’o2’}是沒有問題的,由于直接複制本質上上記憶體指向修改的問題.但是如果我們修改o2.object.test1 = “New value”,此時o1和o2會一起變!!!這種情況是我們不想看到的.對于object、array類型的最好做深拷貝(是否深拷貝看應用場景,讀者需要斟酌),結合我們上面的類型檢測,我們把extend函數修改一下
(function() {
var id =;
function generateId() { return id++; console.log(id)};
Object.prototype.id = function() {
var newId = generateId();
this.id = function() { return newId; }; return newId; };
})();
var isType = function(type){
return function(obj){ return Object.prototype.toString.call(obj) === '[object '+ type +']'; }
}
var is = {
isArray : isType('Array'),
isObject : isType('Object'),
}
var o1 = {
number,
string: "I am a string",
object: { test1: "Old value" },
arr: [ "a string", { test2: "Try changing me" } ]
};
var extend = function(result, source) {
for (var key in source){ var copy = source[key]; if(is.isArray(copy)){ //Array deep copy result[key] = extend(result[key] || [], copy); }else if(is.isObject(copy)){ //Object deep copy result[key] = extend(result[key] || {}, copy); }else{ result[key] = copy; } }
return result;
}
var o2 = extend({},o1);
console.log('o1',o1.number.id());//0
console.log('o1',o1.string.id());//1
console.log('o1',o1.object.id());//2
console.log('o1',o1.arr.id());//3
console.log('o2',o2.number.id());//4
console.log('o2',o2.string.id());//5
console.log('o2',o2.object.id());//6
console.log('o2',o2.arr.id());//7
o2.object.test1 = "new Value";
console.log(o1,JSON.stringify(o1))//o1.object.test1 == "Old value"
console.log(o2,JSON.stringify(o2))//o2.object.test1 == "new Value"
o2.arr[1].test2 = "就不改你";
console.log(o1,JSON.stringify(o1))//o1.object.test1 == "Try changing me"
console.log(o2,JSON.stringify(o2))//o2.arr[1].test2 == "就不改你"
上面内容來自于:
https://yq.aliyun.com/articles/35053