一燕少、什么是淺拷貝鞍陨、什么是深拷貝
我們都知道js的數(shù)據(jù)類型分為基本類型和引用類型,一般討論到淺拷貝和深拷貝的都是針對引用類型的阻塑,像Object和Array這樣的復雜類型废赞,
1、淺拷貝:以Object為例
var a = {
name: 'Wendy'
};
var b = a;
b.name = 'Lily';
console.log(b.name); // Lily
console.log(a.name); // Lily
可以看出叮姑,對于Object類型唉地,當我們將a賦值給b,然后更改b中的屬性传透,a也會隨著變化耘沼。
也就是說a和b指向了同一塊內存,所以修改其中任意的值朱盐,另一個值都會隨之變化群嗤,這就是淺拷貝。
2兵琳、深拷貝
如果給b放到新的內存中狂秘,將a的各個屬性都復制到新內存里骇径,就是深拷貝。
也就是說者春,當b中的屬性有變化的時候破衔,a內的屬性不會發(fā)生變化。
二钱烟、淺拷貝的實現(xiàn)
這里說兩個實現(xiàn)方式:
1晰筛、Object.assign()
用于將所有可枚舉屬性的值從一個或多個源對象復制到目標對象。它將返回目標對象拴袭。
var target = {a: 1, b: 1};
var obj1 = {a: 2, b: 2, c: {ca:1}};
var obj2 = {c: {ca: 3, cb: 2, cd: 1}};
var result = Object.assign(target, obj1, obj2);
console.log(target); // {a: 2, b: 2, c: {ca: 31, cb: 32, cc: 33}}
console.log(target === result); // true
可以看到读第,Object.assign()
拷貝的只是屬性值,假如源對象的屬性值是一個指向對象的引用拥刻,它也只拷貝那個引用值怜瞒。所以Object.assign()
只能用于淺拷貝或是合并對象。這是Object.assign()
值得注意的地方般哼。
2吴汪、函數(shù)實現(xiàn)
function shallowClone(source) {
var target = {};
for(var i in source) {
if (source.hasOwnProperty(i)) {
target[i] = source[i];
}
}
return target;
}
var obj = {a:1, b:2, c:[1,2,3], d:{da:1}}
var clone = shadowClone(obj) //{a:1, b:2, c:[1,2,3], d:{da:1}}
三、深拷貝的實現(xiàn)
1逝她、JSON.parse和JSON.stringify
對于 JSON 安全(也就是說可以被序列化為一個 JSON 字符串并且可以根據(jù)這個字符串解析出一個結構和值完全一樣的對象)的對象來說,有一種巧妙的復制方法:
var clone = JSON.parse(JSON.stringify(target))
當然睬捶,這種方法需要保證對象是 JSON 安全的黔宛,所以只適用于部分情況。
2擒贸、淺拷貝+遞歸
function clone(source) {
var target = {};
for(var i in source) {
if (source.hasOwnProperty(i)) {
if (typeof source[i] === 'object') {
target[i] = clone(source[i]); // 注意這里
} else {
target[i] = source[i];
}
}
}
return target;
}
但是這樣寫還不夠嚴謹臀晃,比如:
- 沒有對參數(shù)做校驗
- 判斷是否是對象的邏輯不夠嚴謹
- 如果用了嚴謹?shù)膶ο笈袛啵敲淳蜎]有考慮到數(shù)組的情況
先看第一個介劫,函數(shù)需要校驗參數(shù)徽惋,如果不是對象直接返回
function clone(source) {
if (!isObject(source))
return source; // xxx
}
第二個typeof校驗實際上只能區(qū)分基本類型和引用類型,其對于Date座韵、RegExp险绘、Array類型返回的是"object"。
目前判斷一個對象類型的最好的辦法是Object.prototype.toString.call()
function isObject(x) {
return Object.prototype.toString.call(x) === '[object Object]';
}
再抽象一些
var types = ["Array", "Boolean", "Date", "Number", "Object", "RegExp", "String", "Window", "HTMLDocument"];
for(var i = 0, c = types[i ];i<types.length;I++ ){
is[c] = (function(type){
return function(obj){
return Object.prototype.toString.call(obj) == "[object " + type + "]";
}
)(c);
}
完善下第三個問題就是
function isObject(x) {return Object.prototype.toString.call(x) === '[object Object]';}
function clone(source) {
var target = {};
if(!isObject(source)) target = source;
else{
for(var i in source) {
if (source.hasOwnProperty(i)) {
if (isObject(source[i]) || Array.isArray(source[i])) {
target[i] = clone(source[i]); // 注意這里
} else {
target[i] = source[i];
}
}
}
}
return target;
}
其實遞歸方法最大的問題在于爆棧誉碴,當數(shù)據(jù)的層次很深,需要同時保存成千上百個調用記錄,很容易發(fā)生"棧溢出"錯誤(stack overflow)
我們用斐波拉契數(shù)列為例响疚,普通遞歸寫法:
function f(n) {
if (n === 0 || n === 1) return n
else return f(n - 1) + f(n - 2)
}
這種寫法国撵,簡單粗暴,但是有個很嚴重的問題成黄。調用棧隨著n的增加而線性增加呐芥,當n為一個大數(shù)時逻杖,就會爆棧了(棧溢出,stack overflow)思瘟。這是因為這種遞歸操作中荸百,同時保存了大量的棧幀,調用棧非常長潮太,消耗了巨大的內存管搪。
三、破解遞歸爆棧
其實破解遞歸爆棧的方法有兩條路铡买,第一種是消除尾遞歸更鲁,但在這個例子中貌似行不通,第二種方法就是干脆不用遞歸奇钞,改用循環(huán)澡为,
1、尾遞歸
要說尾遞歸景埃,就要先了解尾函數(shù)媒至,尾函數(shù)就是指函數(shù)調用最后一步是調用另一個函數(shù)
舉個??:
function f(x){ return g(x);}//屬于尾調用
function f(x){ let y = g(x); return y;}// 不屬于谷徙,因為調用函數(shù)g之后有其它操作
function f(x){ return g(x) + 1;}//不屬于,因為調用后還要其它操作完慧,即使在同一行函數(shù)內
遞歸函數(shù)是調用自身的函數(shù),尾遞歸就是尾調用自身的函數(shù)屈尼,對尾遞歸來說,由于只存在一個調用記錄脾歧,所以永遠不會發(fā)生"棧溢出"錯誤。
簡單解釋下棧溢出問題鞭执,由于函數(shù)調用會在內存形成一個"調用記錄",又稱"調用幀"(call frame)兄纺,保存調用位置和內部變量等信息。如果在函數(shù)A的內部調用函數(shù)B囤热,那么在A的調用記錄上方,還會形成一個B的調用記錄。等到B運行結束锨苏,將結果返回到A,B的調用記錄才會消失伞租。如果函數(shù)B內部還調用函數(shù)C,那就還有一個C的調用記錄棧葵诈,以此類推。所有的調用記錄作喘,就形成一個"調用棧"(call stack)理疙。
尾調用由于是函數(shù)的最后一步操作,所以不需要保留外層函數(shù)的調用記錄泞坦,因為調用位置窖贤、內部變量等信息都不會再用到了,只要直接用內層函數(shù)的調用記錄贰锁,取代外層函數(shù)的調用記錄就可以了赃梧。
接下來,把斐波拉契數(shù)列升級為尾遞歸看看
function fTail(n, a=0, b=1){
if(n===0) return a
else{
return fTail(n-1, b, a+b)
}
}
fTail(5) => fTail(4, 1, 1) => fTail(3, 1, 2) => fTail(2, 2, 3) => fTail(1, 3, 5) => fTail(0, 5, 8) => return 5
被尾遞歸改寫之后的調用棧永遠都是更新當前的棧幀而已豌熄,這樣就完全避免了爆棧的危險
但是授嘀,想法是好的,從尾調用優(yōu)化到尾遞歸優(yōu)化的出發(fā)點也沒錯锣险,但是瀏覽器目前還沒有支持
那么我們可以手動優(yōu)化下:
直接改函數(shù)內部蹄皱,循環(huán)執(zhí)行
function fLoop(n, a = 0, b = 1) {
while (n--) {
[a, b] = [b, a + b]
}
return a
}
這個函數(shù)相對比較簡單,我們把深拷貝代碼用循環(huán)實現(xiàn)下:??
function cloneLoop(x) {
const root = {};
// 棧
const loopList = [
{
parent: root,
key: undefined,
data: x,
}
];
while(loopList.length) {
// 深度優(yōu)先
const node = loopList.pop();
const parent = node.parent;
const key = node.key;
const data = node.data;
// 初始化賦值目標囱持,key為undefined則拷貝到父元素夯接,否則拷貝到子元素
let res = parent;
if (typeof key !== 'undefined') {
res = parent[key] = {};
}
for(let k in data) {
if (data.hasOwnProperty(k)) {
if (typeof data[k] === 'object') {
// 下一次循環(huán)
loopList.push({
parent: res,
key: k,
data: data[k],
});
} else {
res[k] = data[k];
}
}
}
}
return root;
}
以上
參考文章:
You don't know JS(上)第109頁
深拷貝的終極探索
JavaScript 調用棧焕济、尾遞歸和手動優(yōu)化
尾調用優(yōu)化
在簡書上發(fā)布相關文章是對自己不斷學習的激勵纷妆;如有什么寫得不對的地方,歡迎批評指正晴弃;給我點贊的都是小可愛 ~_~