JS中有兩種資料型別,值型別和引用型別,當我們需要把一個變數賦給另一個變數時,對於值型別很簡單:
let a = 1;
let b = a;
b = 10;
console.log(a, b); // 1, 10
複製程式碼
但是如果a是一個物件,這就有問題了
let a = {value: 1};
let b = a;
b.value = 10;
console.log(a.value, b.value); // 10, 10
複製程式碼
我們發現改變b.value
的時候,a.value
的值也跟著變了,這是因為JS裡面的物件是引用型別,我們在把變數a賦值給變數b的時候,賦值過去的其實是a的引用地址,b有了相同的引用地址,那a跟b指向的都是同一塊記憶體空間,操作b的屬性,其實就是操作了這塊記憶體,因為a也指向這塊記憶體,所以a的屬性也變了。這其實就是一個淺拷貝。
淺拷貝
上面這樣我們直接將一個引用變數賦值給另一個變數是一種淺拷貝,淺拷貝其實還有其他形式。這次我們需要拷貝的目標是
let target = {
name: 'John',
age: 20,
friend: {
name: 'Michel',
age: 30
}
}
複製程式碼
我們可以直接遍歷target
物件,將它賦給一個新物件就行。
const shallowCopy = (obj) => {
// 判斷引數是陣列還是物件
const result = Array.isArray(obj) ? [] : {};
for(let key in obj) {
// 使用hasOwnProperty來判斷是否是自身屬性
// 只拷貝自身屬性,不拷貝原型鏈上的屬性,即繼承屬性
if(obj.hasOwnProperty(key)){
result[key] = obj[key];
}
}
return result;
}
複製程式碼
然後我們來用一下這個方法:
let newObj = shallowCopy(target);
newObj.age = 50;
console.log(target.age, newObj.age); //20, 50
複製程式碼
我們可以看到當我們改變newObj的屬性時,原物件的屬性並沒有受影響,但是如果我們改變newObj.friend
呢?
newObj.friend.age = 50;
console.log(target.friend.age, newObj.friend.age); //50, 50
複製程式碼
我們發現當我們改變newObj.friend
的屬性的時候,原物件的newObj.friend
的屬性也改變了,這是因為target.friend
本身也是一個物件,我們拷貝的時候只拷貝了他的引用地址,所以我們通過newObj
操作他的時候也改變了原來的target
。
從上面可以看出我們的shallowCopy
方法只拷貝了物件的一層,這也是一種淺拷貝。其實還有一些原生方法也是隻拷貝一層的,比如Object.assign
和...
擴充套件運算子
let newObj = Object.assign({}, target); // 這是一層的淺拷貝
let newObj = {...target}; // 這也是一層的淺拷貝
複製程式碼
那深拷貝應該怎麼實現呢?
深拷貝
JSON
最簡單的實現方法就是用JSON.stringify
先將物件轉換為字串,然後再用JSON.parse
重新解析為JSON,這樣新生成的物件與原物件就完全沒有關係了,還是以前面的target
為例:
let newObj = JSON.parse(JSON.stringify(target));
newObj.friend.age = 50;
console.log(target.friend.age, newObj.friend.age); //30, 50
複製程式碼
但是我們換一個target
再來試試:
let target2 = {
name: 'John',
age: 20,
drive: () => {},
girlFriend: undefined
}
let newObj = JSON.parse(JSON.stringify(target2));
console.log(newObj);
複製程式碼
結果如下圖,我們發現drive
和girlFriend
兩個屬性都丟了,這是因為JSON.stringify
不能將方法和undefined
屬性轉化為字串,在轉換為字串過程中就丟了,再解析回來自然也沒有了。
遞迴遍歷
要解決上面的問題,我們還要自己動手,我們改造下上面的shallowCopy
方法,讓他能夠遞迴複製。
const deepCopy = (obj) => {
const result = Array.isArray(obj) ? [] : {};
for(let key in obj) {
if(obj.hasOwnProperty(key)){
// 如果屬性也是物件,遞迴呼叫自身
if(obj[key] && typeof obj[key] === 'object'){
result[key] = deepCopy(obj[key])
} else {
result[key] = obj[key];
}
}
}
return result;
}
複製程式碼
來看下結果:
let newObj = deepCopy(target2);
console.log(newObj);
複製程式碼
這下我們的drive
方法和girlFriend
屬性都複製過來了。
拷貝Symbol
那如果換一個帶有Symbol
屬性的物件呢?
let target3 = {
[Symbol('name')]: 'John',
age: 20,
drive: () => {},
girlFriend: undefined
}
複製程式碼
我們來看看結果:
let newObj = deepCopy(target3);
console.log(newObj);
複製程式碼
我們發現Symbol
屬性丟了,那怎麼辦呢?這個原因是for...in...
迴圈拿不到Symbol
屬性,如果要拿Symbol
屬性,我們可以用Object.getOwnPropertySymbols
和Reflect.ownKeys
。Object.getOwnPropertySymbols
會返回物件的Symbol
屬性列表:
Reflect.ownKeys
會返回物件的所有自有屬性,包括Symbol
屬性和不可列舉屬性,但是不包括繼承屬性。所以我們的deepCopy
方法改為:
const deepCopy = (obj) => {
const result = Array.isArray(obj) ? [] : {};
// 用 Reflect.ownKeys可以獲取Symbol屬性,用for...of來迴圈陣列
for(let key of Reflect.ownKeys(obj)) {
if(obj.hasOwnProperty(key)){
if(obj[key] && typeof obj[key] === 'object'){
result[key] = deepCopy(obj[key])
} else {
result[key] = obj[key];
}
}
}
return result;
}
複製程式碼
再來看看結果:
let newObj = deepCopy(target3);
console.log(newObj);
複製程式碼
解決迴圈引用
我們來考慮一個新的目標物件
let target4 = {
[Symbol('name')]: 'John',
age: 20,
drive: () => {},
girlFriend: undefined
}
target4.target = target4;
複製程式碼
這個物件的target
屬性又引用了自身,所以有了迴圈引用,用我們之前的深拷貝方法直接會報錯
要解決這個問題,我們需要每次都將引用型別的鍵和值都記錄下來,由於Object的鍵不能是物件,所以我們不能用Object記錄,這裡採用了WeakMap來記錄:
const deepCopy2 = (originObj) => {
// 全域性只能有一個記錄的map,所以裡面又嵌了一個方法
const map = new WeakMap();
function dp(obj){
const result = Array.isArray(obj) ? [] : {};
const existObj = map.get(obj);
// 檢查map中是不是已經有這個物件了,有了就直接返回,不再遞迴
if(existObj){
return existObj;
}
// 沒有就記錄下來
map.set(obj, result);
for(let key of Reflect.ownKeys(obj)) {
if(obj.hasOwnProperty(key)){
if(obj[key] && typeof obj[key] === 'object'){
result[key] = dp(obj[key])
} else {
result[key] = obj[key];
}
}
}
return result;
}
return dp(originObj);
}
複製程式碼
WeakMap
的相容性不是很好,如果是老瀏覽器不支援WeakMap
,我們可以用兩個陣列來模擬,一個陣列存鍵,一個陣列存值,每次都只在兩個陣列末尾新增值,這樣鍵和值在陣列中的索引就是一樣的,我們可以通過這個索引來進行鍵和值的匹配。
淺拷貝的應用:mixin--混合模式
直接看程式碼
const mixin = {
// 注意:這裡的say和run不能寫成箭頭函式,因為箭頭函式拿不到正確的this
say() {
console.log(`${this.name}在說話`)
},
run() {
console.log(`${this.name}在跑步`)
}
}
class Student{
constructor(name){
this.name = name
}
}
Object.assign(Student.prototype, mixin);
const student1 = new Student('Jhon');
student1.say();
複製程式碼
上面的程式碼我們沒有用繼承,而是用了拷貝的方式,讓Student
類具有了mixin的方法,我們直接將mixin裡面的方法複製到了Student
的原型鏈上。這種模式在很多地方都有應用,比如Vue:
深拷貝應用:pick函式
在underscore裡面有一個pick函式,可以實現如下效果:
上述程式碼的輸出是一個只包含age
屬性的新物件{age: 30}
,下面讓我們自己來實現一個pick函式,實現在原理很簡單,把我們之前深拷貝的方法改一下就行,讓他只拷貝我們需要的屬性:
const pick = (originObj, property) => {
const map = new WeakMap();
function dp(obj){
const result = Array.isArray(obj) ? [] : {};
const existObj = map.get(obj);
if(existObj){
return existObj;
}
map.set(obj, result);
for(let key of Reflect.ownKeys(obj)) {
// 只需要加一個檢測,看看key是不是我們需要的屬性就行
if(obj.hasOwnProperty(key) && key === property){
if(obj[key] && typeof obj[key] === 'object'){
result[key] = dp(obj[key])
} else {
result[key] = obj[key];
}
}
}
return result;
}
return dp(originObj);
}複製程式碼
原創不易,每篇文章都耗費了作者大量的時間和心血,如果本文對你有幫助,請點贊支援作者,也讓更多人看到本文~~
更多文章請看我的掘金文章彙總