說說JS中的淺拷貝與深拷貝
在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 = [3, 4, 5];
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: [0, 1, 2]
};
var cloneObj = simpleClone(obj);
console.log(cloneObj.name);
console.log(cloneObj.val);
//cloneObj.name = "simpleCloneTest2";
//cloneObj.val = [3, 4, 5];
//console.log(cloneObj.name);
//console.log(cloneObj.val);
//console.log(obj.name);
//console.log(obj.val);
cloneObj.name = "simpleCloneTest3";
cloneObj.val[0] = 3;
console.log(cloneObj.name);
console.log(cloneObj.val[0]);
console.log(obj.name);
console.log(obj.val[0]);
輸出結果如下:
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 = 0;
function generateId() { return id++; console.log(id)};
Object.prototype.id = function() {
var newId = generateId();
this.id = function() { return newId; };
return newId;
};
})();
var a = 1;
console.log(a.id());//0
a = 2;
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 = 0;
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 = 0;
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 = 0;
function generateId() { return id++; console.log(id)};
Object.prototype.id = function() {
var newId = generateId();
this.id = function() { return newId; };
return newId;
};
})();
var o1 = {
number: 1,
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 = 0;
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: 1,
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 == "就不改你"