簡單講呢,深淺拷貝,都是進行復制,那么區別主要在于復制出來的新對象和原來的對象是否會互相影響,改一個,另一個也會變。
學過c,c++或者Java這類的語言的人都知道這是什么回事,理解起來自然沒什么困難,不過沒學過的也沒關系,咱們看下面的分析
先看兩個例子
- 淺拷貝例子:
var a = ["a","b","c"];
var a_copy = a;
console.log(a === a_copy);
a_copy[0]="f";
console.log(a_copy);
console.log(a);
結果是不是出乎你的意料?按常理說,我把a賦給a_copy后改變a_copy , a應該不會變啊
- 再看深拷貝例子:
var obj = {name:"xixi",age:20};
var obj_extend = Object.assign({}, obj); //Object.assign ES6方法,拷貝的是屬性值
console.log(obj === obj_extend);
obj.name = "heihei";
console.log(obj);
console.log(obj_extend);
- 關于Object.assign的用法請戳這里
還有一點,js中Array 和 Object的==,===比較都是比較它們在內存中的地址
到這里我們來了解下js的深淺拷貝原理
一、基本類型 和 引用類型
1、ECMAScript 中的變量類型分為兩類:
- 基本類型:undefined,null,布爾值(Boolean),字符串(String),數值(Number)
- 引用類型: 統稱為Object類型,細分的話,有:Object類型,Array類型,Date類型,Function類型等。
2、不同類型的存儲方式:
基本數據類型 保存在 棧內存,形式如下:棧內存中分別存儲著變量的標識符以及變量的值。
即
var a = 'A'
棧內存中是這樣的
引用類型 保存在 堆內存 中, 棧內存存儲的是變量的標識符以及對象在堆內存中的存儲地址,當需要訪問引用類型(如對象,數組等)的值時,首先從棧中獲得該對象的地址指針,然后再從對應的堆內存中取得所需的數據。
var a = {name:"jack"}
在內存中是這樣的
3、不同類型的復制方式:
基本類型的復制:當你在復制基本類型的時候,相當于把值也一并復制給了新的變量。
- 例子1:
var a = 1;
var b = a;
console.log(a === b);
var a = 2;
console.log(a);
console.log(b);
改變 a 變量的值,并不會影響 b 的值。
內存中是這樣的:
var a = 1;
var b = a;
a = 2;
引用類型的復制:當你在復制引用類型的時候,實際上只是復制了指向堆內存的地址,即原來的變量與復制的新變量指向了同一個東西。
- 例子2:
var a = {name:"jack",age:20};
var b = a;
console.log(a === b);
a.age = 30;
console.log(a);
console.log(b);
改變 a 變量的值,會影響 b 的值。
內存中是這樣的:
var a = {name:“jack",age:20};
var b = a;
a.age = 30;
二、明白了上面之后,所謂 深淺拷貝:
對于僅僅是復制了引用(地址),換句話說,復制了之后,原來的變量和新的變量指向同一個東西,彼此之間的操作會互相影響,為 淺拷貝。
而如果是在堆中重新分配內存,擁有不同的地址,但是值是一樣的,復制后的對象與原來的對象是完全隔離,互不影響,為 深拷貝。
深淺拷貝的主要區別就是:復制的是引用(地址)還是復制的是實例。
所以上面的栗子2,如何可以變成深拷貝呢?
我們可以想象出讓 b 在內存中像下圖這樣,肯定就是深拷貝了。
那么代碼上如何實現呢?
利用 遞歸 來實現深復制,對屬性中所有引用類型的值,遍歷到是基本類型的值為止。
function deepClone(source){
if(!source && typeof source !== 'object'){ //不存在或者不是Object就拋出錯誤
throw new Error('error arguments');
}
var targetObj = source.constructor === Array ? [] : {}; //對于Object類型檢測只能使用該方法Constructor:對創建對象的函數的引用(指針);對于Object類,該指針指向原始的object()函數
for(var keys in source){ //遍歷source
if(source.hasOwnProperty(keys)){ //檢查source是否有keys這個屬性
if(source[keys] && typeof source[keys] === 'object'){ //
targetObj[keys] = source[keys].constructor === Array ? [] : {};
targetObj[keys] = deepClone(source[keys]); //遞歸 ,對象結構很像一棵樹,樹的一種遍歷方法就是遞歸
}else{
targetObj[keys] = source[keys];
}
}
}
return targetObj;
}
檢測一下
var a = {name:"jack",age:20};
var b = deepClone(a);
console.log(a === b);
a.age = 30;
console.log(a);
console.log(b);
三 、最后讓我們來看看 一些 js 中的 復制方法,他們到底是深拷貝還是淺拷貝?
1、 Array 的 slice 和 concat 方法
兩者都會返回一個新的數組實例。
- 例子
var a = [1,2,3];
var b = a.slice(); //slice
console.log(b === a);
a[0] = 4;
console.log(a);
console.log(b);
var a = [1,2,3];
var b = a.concat(); //參數為空,相當于使用contact方法復制了a數組賦值給b
console.log(b === a);
a[0] = 4;
console.log(a);
console.log(b);
看到結果,如果你心中竊喜這不就是深拷貝嗎?,那就恭喜你跳進了坑里
讓咱們再看一個顛覆你觀念的例子:
var a = [[1,2,3],4,5];
var b = a.slice();
console.log(a === b);
a[0][0] = 6;
console.log(a);
console.log(b);
看見了嗎?都變啦!!!!
這就是坑,知道嗎????
到這你會驚訝怎么前面幾個例子都是深拷貝,到這怎么變成淺拷貝了
其實總結一下就是:
Array 的 slice 和 concat 方法 和,他們都會復制第一層的值,對于 第一層的值都是 深拷貝,而到 第二層的時候 Array 的 slice 和 concat 方法就是 復制引用
3、JSON 對象的 parse 和 stringify
JOSN 對象中的 stringify 可以把一個 js 對象序列化為一個 JSON 字符串,parse 可以把 JSON 字符串反序列化為一個 js 對象,這兩個方法實現的是深拷貝。
- 例子:
var obj = {name:'aa',age:20,company : { name : 'ff', address : 'dd'} };
var obj_json = JSON.parse(JSON.stringify(obj));
console.log(obj === obj_json);
obj.company.name = "b";
obj.name = "c";
console.log(obj);
console.log(obj_json);