克隆(也就是拷貝)是javascript中很重要也很常見的問題。克隆就是將一個物件裡的屬性、方法等複製到另一個物件中,且互不影響(即克隆之後,對一個物件進行改動,不會影響到另一物件)。我們今天就來討論一下原生js中克隆的問題。
action~
現在有一個物件
var obj = {
name: '隔壁老王',
age: 60,
sex: 'male'
}
複製程式碼
我們現在想把obj裡的每一個屬性拷貝到一個空物件var obj1 = {}中,那麼需要寫一個克隆方法,首先遍歷一下obj物件,然後把裡面的每一個屬性都拷貝過去。程式碼如下:
var obj = {
name: '隔壁老王',
age: 60,
sex: 'male'
};
var obj1 = {};
function clone(origin, target) {
for (var prop in origin) {
target[prop] = origin[prop];
}
};
clone(obj, obj1);
複製程式碼
在控制檯中檢視obj1,會發現實現了對obj的拷貝。
我們再對這個方法進行一些完善:有可能使用者在執行clone方法時,只傳origin一個引數,然後將函式執行結果賦給物件obj1,所以我們需要在方法最下方加一個返回值target,返回克隆結果,相應的也要在函式體裡宣告var target = {};
。
還有一種情況obj1物件裡事先有其他屬性,那麼函式體裡寫var target = {};
顯然是不符合預期的,所以要完善成var target = target || {};
確保萬無一失。最終該方法完善為:
function clone(origin, target) {
var target = target || {};
for (var prop in origin) {
target[prop] = origin[prop];
}
return target;
}
複製程式碼
下面對obj進行拷貝,且obj1本身有自己的屬性,執行程式碼如下:
var obj = {
name: '隔壁老王',
age: 60,
sex: 'male'
};
var obj1 = {
father: '老王'
};
function clone(origin, target) {
var target = target || {};
for (var prop in origin) {
target[prop] = origin[prop];
}
return target;
}
clone(obj, obj1);
複製程式碼
檢視一下拷貝結果
完美拷貝了obj的所有屬性,又保留了自身屬性。 到這就結束了麼?要知道js有六大資料型別(本文不討論ES6新增的Symbol型別):number,string,boolean,undefined,null,object。其中number,string,boolean,undefined,null歸為原始值一類,而object屬於引用值,具體包括狹義的object,array,function。
下面往obj物件里加點引用值,用我們已經寫出來的克隆方法試試:
var obj = {
name: '隔壁老王',
age: 60,
sex: 'male',
card: ['信用卡', '借記卡', '理髮卡'],
wife: {
name: '小劉'
},
divorce: function () { }
};
var obj1 = {
father: '老王'
};
function clone(origin, target) {
var target = target || {};
for (var prop in origin) {
target[prop] = origin[prop];
}
return target;
}
clone(obj, obj1);
複製程式碼
貌似拷貝成功了
當我們對obj物件進行一波這樣的操作: 再檢視obj1會發現 在obj1裡也多了美容卡和一個兒子,函式沒受影響。我們可以得出這樣的結論:對於陣列和物件,用上面那種克隆方法是不行的。因為對於陣列和物件而言,拷貝的是地址,他們指向的都是同一個空間,通過一個物件在這個空間裡面加了東西,另一個物件必然也會受到影響。
而對於函式而言,通過上面這種普通的賦值拷貝,就可以實現,且互不影響,因為函式的克隆會在記憶體中單獨開闢一塊空間。
我們管上面寫的這種克隆方法叫淺度克隆,它可應用於不包含物件(狹義的)和陣列的物件之間的拷貝。有點繞哈~
下面我們來解決一下狹義的物件和陣列的拷貝問題,即我們需要另一種萬全之策——深度克隆。
先來整理下思路:
1.遍歷待拷貝的物件; 2.判斷每個元素是不是原始值,若是,則通過淺度克隆的手段進行拷貝; 3.若是引用值,則需要繼續判斷是物件還是陣列; 4.再分別建立空陣列或物件用來盛放裡面即將拷貝而來的屬性值; 5.陣列和物件裡面的若是原始值,則淺拷貝即可實現,若還有引用值,則還需要重複進行上述一系列的判斷。
上述每一步思路怎麼用程式碼實現呢:
1.使用for in進行遍歷。但需要注意的是,for in方法會把物件原型裡的屬性也一起遍歷了,所以要與hasOwnProperty()方法進行聯用,hasOwnProperty()方法可以判斷某屬性是不是該物件自己的屬性,從而過濾掉原型中的屬性。 2.用typeof()返回值來進行判斷,陣列和物件的typeof返回值是'object'。 3.判斷物件還是陣列有多種方法,舉出常見的三種:分別是constructor、instanceof和toString()方法。這裡最好用toString()方法,因為在有父子域之間拷貝的情況,constructor和instanceof這兩種是不好用的。 4.就是[]和{}唄。 5.重複判斷,自然想到遞迴。
深度克隆方法如下:
function deepClone(origin, target) {
var target = target || {},
toStr = Object.prototype.toString,
arrStr = '[object Array]';
for (var prop in origin) {
if (origin.hasOwnProperty(prop)) {
if (typeof (origin[prop]) == 'object' && origin[prop] !== null) {
if (toStr.call(origin[prop]) == arrStr) {
target[prop] = [];
} else {
target[prop] = {};
}
deepClone(origin[prop], target[prop]);
} else {
target[prop] = origin[prop];
}
}
}
return target;
}
複製程式碼
注意到origin[prop] !== null
這句了麼?為啥要加上它,因為typeof(null)也是'object'。
來試試吧,我們把obj變得複雜一點,給隔壁老王的老婆增加倆兒子:王小寶和王二寶。執行過程如下:
var obj = {
name: '隔壁老王',
age: 60,
sex: 'male',
card: ['信用卡', '借記卡', '理髮卡'],
wife: {
name: '小劉',
son: {
name1: '王小寶',
name2: '王二寶'
}
},
divorce: function () { }
};
var obj1 = {
father: "老王"
};
function deepClone(origin, target) {
var target = target || {},
toStr = Object.prototype.toString,
arrStr = '[object Array]';
for (var prop in origin) {
if (origin.hasOwnProperty(prop)) {
if (typeof (origin[prop]) == 'object' && origin[prop] !== null) {
target[prop] = (toStr.call(origin[prop]) == arrStr) ? [] : {};
deepClone(origin[prop], target[prop]);
} else {
target[prop] = origin[prop];
}
}
}
return target;
}
deepClone(obj, obj1);
複製程式碼
看看結果,沒毛病。
對obj的引用值增加一些屬性試試。 發現obj1紋絲不動,完美拷貝實現!綜上,克隆方法白話完了,實戰開發中針對實際需要採取不同的克隆手段。