淺談JavaScript的淺拷貝與深拷貝
資料型別
在開始拷貝之前,我們從JavaScript的資料型別和記憶體存放地址講起。
資料型別分為基本資料型別 和引用資料型別
基本資料型別主要包括undefined,boolean,number, string,null。
基本資料型別主要存放在棧(stack),存放在棧中的資料簡單,大小確定。存放在棧記憶體中的資料是直接按值存放的,是可以直接訪問的。
基本資料型別的比較是值的比較,只要它們的值相等就認為它們是相等的。
let a = 1;
let b = 1;
console.log(a === b); //true
這裡使用嚴格相等,主要是為了==會進行型別轉換。
let a = true; let b = 1; console.log(a == b); //true
引用資料型別也就是物件型別Object type,比如object,array, function,date等。
引用資料型別是存放在堆(heap)記憶體中的,變數實際上是一個存放在棧記憶體的指標,這個指標指向堆記憶體中的地址。
引用資料型別的比較是引用的比較
所以我們每次對js中的引用型別進行操作的時候,都是操作其儲存在棧記憶體中的指標,所以比較兩個引用資料型別,是看它們的指標是否指向同一個物件。
let foo = {a: 1, b: 2};
let bar = {a: 1, b: 2};
console.log(foo === bar); //false
雖然變數foo和變數bar所表示的內容是一樣的,但是其在記憶體中的位置不一樣,也就是變數foo和bar在棧記憶體中存放的指標指向的不是堆記憶體中的同一個物件,所以它們是不相等的。
棧和堆的區別
其實淺拷貝和深拷貝的主要區別就是資料在記憶體中的儲存型別不同。
棧和堆都是記憶體中劃分出來用來儲存的區域。
棧(stack) 是自動分配的記憶體空間,由系統自動釋放;
堆(heap) 則是動態分配的記憶體,大小不定也不會自動釋放。
淺拷貝
如果你的物件只有值型別的屬性,可以用ES6的新語法Object.assign(...)實現拷貝
//淺拷貝
let obj = {foo: 'foo', bar: "bar"};
let shallowCopy = { ...obj }; //{foo: 'foo', bar: "bar"}
//淺拷貝 let obj = {foo: "foo", bar: "bar"}; let shallowCopy = Object.assign({}, obj); //{foo: 'foo', bar: "bar"}
我們接著來看下淺拷貝和賦值(=) 的區別
let obj = {foo: "foo", bar: "bar"};
let shallowCopy = { ...obj }; //{foo: 'foo', bar: "bar"}
let obj2= obj; //{foo: 'foo', bar: "bar"}
shallowCopy.foo = 1;
obj2.bar = 1
console.log(obj); //{foo: "foo", bar: 1};
可以看出賦值得到的obj2和最初的obj指向的是同一物件,改變資料會使原資料一同改變。
而淺拷貝得到的shallowCopy則將obj的第一層資料物件拷貝到了,和源資料不指向同一物件,改變不會使原資料一同改變。
深拷貝
但是Object.assign(...)方法只能進行物件的一層拷貝。對於物件的屬性是物件的物件,他不能進行深層拷貝。
迷糊了吧?直接程式碼解釋
let foo = {a: 0, b: {c: 0}};
let copy = { ...foo };
copy.a = 1;
copy.b.c = 1;
console.log(copy); //{a: 1, b: {c: 1}};
console.log(foo); //{a: 0, b: {c: 1}};
可以看到,使用Object.assign(...)方法拷貝的copy物件的二層物件發生改變的時候,依然會使原資料一同改變。
這裡,對存在子物件的物件進行拷貝的時候,就是深拷貝了。
淺拷貝:將B物件拷貝到A物件中,不包括B裡面的子物件
深拷貝:將B物件拷貝到A物件中,包括B裡面的子物件
深拷貝實現的方法:
這裡只說幾種常用方法,
1.JSON.parse(JSON.stringify( ));
let foo = {a: 0, b: {c: 0}};
let copy = JSON.parse(JSON.stringify(foo));
copy.a = 1;
copy.b.c = 1;
console.log(copy); //{a: 1, b: {c: 1}};
console.log(foo); //{a: 0, b: {c: 0}};
2.遞迴拷貝
function deepCopy(initialObj, finalObj){
let obj = finalObj || {};
for(let i in initialObj) {
if(typeof initialObj[i] === "object") {
obj[i] = (initialObj[i].constructor === Array) ? [] : {};
arguments.callee(initialObj[i], obj[i]);
}else{
obj[i] = initialObj[i];
}
}
return obj;
}
var foo = {a: 0, b: {c: 0}};
var str = {};
deepCopy(foo, str);
str.a = 1;
str.b.c = 1;
console.log(str); //{a: 1, b: {c: 1}};
console.log(foo); //{a: 0, b: {c: 0}};
上述程式碼確實可以實現深拷貝,但是當遇到兩個互相引用的物件,會出現死迴圈的情況。
為了避免相互引用的物件導致死迴圈的情況,則應該在遍歷的時候判斷是否相互引用物件,如果是則退出迴圈。
改進版程式碼如下:
function deepCopy(initialObj, finalObj) {
let obj = finalObj || {};
for(let i in initialObj){
let prop = initialObj[i];//避免相互引用導致死迴圈,如initialObj.a = initialObj的情況
if(prop === obj) {
continue;
}
if(typeof prop === 'object'){
obj[i] = (prop.constructor === Array) ? [] : {};
arguments.callee(prop, obj[i]);
}else{
obj[i] = prop;
}
}
return obj;
}
var foo = {a: 0, b: {c: 0}};
var str = {};
deepCopy(foo, str);
str.a = 1;
str.b.c = 1;
console.log(str); //{a: 1, b: {c: 1}};
console.log(foo); //{a: 0, b: {c: 0}};
3.使用Object.create( )方法
function deepCopy(initialObj, finalObj){
let obj = finalObj || {};
for(let i in initialObj){
let prop = initialObj[i]; //避免相互引用物件導致死迴圈,如initialObj[i].a = initialObj的情況
if(prop === obj){
continue;
}
if(typeof prop === "object"){
obj[i] = (prop.constructor === Array) ? [] : Object.create(prop);
}else{
obj[i] = prop;
}
}
return obj;
}
let foo = {a: 0, b: {c: 0}};
let str = {};
deepCopy(foo, str);
str.a = 1;
str.b.c = 1;
console.log(str); //{a: 1, b: {c: 1}};
console.log(foo); //{a: 0, b: {c: 0}};
4.jQuery
jQuery提供了一個$.extend可以實現深拷貝
var $ = require('jquery);
let foo = {a: 0, b: {c: 0}};
let str = $.extend(true, {}, foo);
str.a = 1;
str.b.c = 1;
console.log(str); //{a: 1, b: {c: 1}};
console.log(foo); //{a: 0, b: {c: 0}};
5.lodash
另一個很熱門的函式庫lodash,也有提供_.cloneDeep用來深拷貝
var _ = require('lodash);
let foo = {a: 0, b: {c: 0}};
let str = _.cloneDeep(foo);
str.a = 1;
str.b.c = 1;
console.log(str); //{a: 1, b: {c: 1}};
console.log(foo); //{a: 0, b: {c: 0}};
侷限性
所有深拷貝的方法並不適用於所有型別的物件。當然還有其他的坑,像是如何拷貝原型鏈上的屬性?如何拷貝不可列舉屬性等等。
雖然lodash是最安全的通用深拷貝方法,但如果你自己動手,可能會依據需求寫出最適合你的更高效的深拷貝的方法:
//適用於日期的簡單深拷貝的例子
function deepCopy(obj) {
let copy;
//處理三種簡單的引用資料型別加上undefined和null
if(obj == null || typeof obj != "object") return obj;
//處理Date
if(obj instanceof Date){
copy = new Date();
copy.setTime(obj.getTime());
return copy;
}
//處理Array
if(obj instanceof Array) {
copy = [];
for(let i = 0; i < obj.length; i++) {
copy[i] = deepCopy(obj[i]);
}
return copy;
}
//處理Function
if(obj instanceof Function) {
copy = function() {
return obj.apply(this, arguments);
}
return copy;
}
//處理Object
if(obj instanceof Object) {
copy = {};
for(let attr in obj) {
if(obj.hasOwnProperty(attr)) copy[attr] = deepCopy(obj[attr]);
}
return copy;
}
throw new Error("無法深拷貝" +obj.constructor+ "型別的資料")
}
快樂拷貝