前言
工作中會經常遇到操作數組、對象的情況,你肯定會將原數組、對象進行‘備份’
當真正對其操作時發現備份的也發生改變,此時你一臉懵逼,到時是為啥,不是已經備份了么,怎么備份的數組、對象也會發生變化。
如果你對拷貝原理理解的不透徹,此文或許能提供一點幫助。
javascript數據類型
基本數據類型
string、number、null、undefined、boolean、symbol(ES6新增) 變量值存放在棧內存中,可直接訪問和修改變量的值
基本數據類型不存在拷貝,好比如說你無法修改數值1的值
引用類型
Object Function RegExp Math Date 值為對象,存放在堆內存中
在棧內存中變量保存的是一個指針,指向對應在堆內存中的地址。
當訪問引用類型的時候,要先從棧中取出該對象的地址指針,然后再從堆內存中取得所需的數據
淺拷貝
為什么備份的數組對象也會發生變化,這里就涉及到你用的‘備份’其實是一種淺拷貝
簡單的引用拷貝
var a = [1,2,3,4]; var b = a; a[0] = 0; console.log(a,b);
可以看到數組a直接賦值給b,a、b引用的其實是一個對象地址,只要地址值發生變化,a、b棧內存指針指向的堆地址也會發生變化,這種引用拷貝只是新增了一個變量棧內存的指針,意義不大
數組的concat、slice,對象的assign拷貝
同樣的例子
var a = [1,2,3,4]; var b = a.concat(); a[0] = 0; console.log(a,b);
此時數組a[0]值變成0,b數組依然保持不變,有同學就問了,這不就是深拷貝嗎。
對,也不對, Array.prototype.slice 和 Array.prototype.concat 看似深拷貝,其實質上還是淺拷貝
var a = [1,2,[3,4],{name:'ccy'}]; var b = a.concat(); a[3].name = 'hs'; console.log(a[3],b[3]);
當數組a中包含對象時, Array.prototype.slice 和 Array.prototype.cancat 拷貝出來數組中的對象還是共享同一內存地址,所以本質上歸屬淺拷貝
Object.assign 原理也是一樣的(對於對象的屬性都為基本類型可以當成深拷貝)
var a = {age:18,name:'ccy',info:{address:'wuhan',interest:'playCards'}}; var b = Object.assign(a); a.info.address = 'shenzhen'; console.log(a.info,b.info);
那怎樣才能對對象進行深拷貝呢,請扶好眼鏡。
自定義深拷貝函數
var clone = function(obj){ var construct = Object.prototype.toString.call(obj).slice(8,-1); var res; if(construct === 'Array'){ res = []; }else if(construct === 'Object'){ res = {} } for(var item in obj){ if(typeof obj[item] === 'object'){ res[item] = clone(obj[item]); }else{ res[item] = obj[item]; } } return res; };
乍一看好像能處理對象的屬性為對象的問題,可以循環遍歷直至屬性為基本類型;
但是仔細想,如果遇到對象的屬性存在相互引用的話會出現死循環的情況。可以再加一次判斷,對象的屬性如果引用對象指針則跳出當前循環。
深拷貝
深拷貝是可以完美的解決淺拷貝的弊端,重新開辟一塊地址,深拷貝出來的屬性的基本類型值都是相同的。
JSON內置對象深拷貝
JSON 對象是ES5中引入的新的類型(支持的瀏覽器為IE8+),JSON對象parse方法可以將JSON字符串反序列化成JS對象,stringify方法可以將JS對象序列化成JSON字符串,借助這兩個方法,也可以實現對象的深拷貝
var a = {age:1,name:'ccy',info:{address:'wuhan',interest:'playCards'}}; var b = JSON.parse(JSON.stringify(a)); a.info.address = 'shenzhen'; console.log(a.info,b.info);
JSON 可處理一般的對象進行深拷貝,但是不能處理函數、正則等對象
我們可以對自定義的拷貝函數再進行優化
var clone = function(obj){ function getType(obj){ return Object.prototype.toString.call(obj).slice(8,-1); } function getReg(a){ var c = a.lastIndexOf('/'); var reg = a.substring(1,c); var escMap = {'"': '\\"', '\\': '\\\\', '\b': '\\b', '\f': '\\f', '\n': '\\n', '\r': '\\r', '\t': '\\t', '\w': '\\w', '\s': '\\s', '\d': '\\d'}; for(var i in escMap){ if(reg.indexOf(i)){ reg.replace(i,escMap[i]); } } var attr = a.substring(c+1); return new RegExp(reg, attr); } var construct = getType(obj); var res; if(construct === 'Array'){ res = []; }else if(construct === 'Object'){ res = {} } for(var item in obj){ if(obj[item] === obj) continue;//存在引用則跳出當前循環 if(getType(obj[item]) === 'Function'){ res[item] = new Function("return "+obj[item].toString())(); }else if(getType(obj[item]) === 'RegExp'){ res[item] = getReg(obj[item].toString()); }else if(getType(obj[item]) === 'Object'){ res[item] = clone(obj[item]); }else{ res[item] = obj[item]; } } return res; };
基本可以實現函數、正則對象的深拷貝,在本地只做了簡單的測試,如果存在問題,請及時評論指出。
當然像函數庫 lodash 的 _.cloneDeep、 JQuery 的 $.extend都實現了深拷貝,有興趣的同學可自行看下源碼。
參考資料
https://developer.mozilla.org/zh-CN/search?q=%E6%B7%B1%E6%8B%B7%E8%B4%9D