拷貝的問題主要是針對引用類型javascript
對於這個問題,首先讓咱們先簡單回顧一下 JavaScript 的基本知識vue
一、JavaScript 包含兩種不一樣數據類型的值:基本類型(原始值)
和 引用類型
java
基本類型有如下幾種,具體以下:
string、number、boolean、null、undefined、symbol、bigIntgit引用類型具體有:
Object(Object、Array、Function...)github
在將一個值賦給變量時,解析器必須肯定這個值是基本類型仍是引用類型算法
二、JavaScript 的變量存儲方式 -- 棧(stack)
和 堆(heap)
segmentfault
棧
:自動分配內存空間,系統自動釋放,裏面存放的是基本類型的值和引用類型的地址指針堆
:動態分配內存,大小不定,也不會自動釋放,裏面存放引用類型的值三、JavaScript 值傳遞與址傳遞
基本類型與引用類型最大的區別實際就是傳值與傳址的區別數組
let a = 1;
let b = a;
b++;
console.log(a, b) // 1, 2
複製代碼
let a = ['a', 'b', 'c'];
let b = a;
b.push('d');
console.log(a) // ['a', 'b', 'c', 'd']
console.log(b) // ['a', 'b', 'c', 'd']
複製代碼
分析:markdown
那麼如何解決上面出現的問題,這裏就引出了淺拷貝或者深拷貝了。JS 的基本類型不存在淺拷貝仍是深拷貝的問題,主要是針對引用類型數據結構
淺拷貝
:拷貝的級別淺。淺拷貝是指複製對象時只對第一層鍵值對進行復制,若對象內還有對象則只能複製嵌套對象的地址指針
深拷貝
:拷貝級別更深。深拷貝是指複製對象時是徹底拷貝,即便嵌套了對象,拷貝後二者也相互不影響,修改一個對象的屬性不會影響另外一個。原理實際上是遞歸把那些值是對象的屬性再次進入對象內部進行復制
slice
、concat
如果數組,數組元素均爲基本數據類型,可利用數組的一些方法如 slice
、concat
返回一個新數組的特性來實現拷貝(此時至關於深拷貝)
若數組的元素是引用類型(Object,Array),slice
和 concat
對對象數組的拷貝仍是淺拷貝,拷貝以後數組各個元素的指針仍是指向相同的存儲地址
let arr = ['one', 'two', 'three'];
let newArr = arr.concat();
newArr.push('four')
console.log(arr) // ["one", "two", "three"]
console.log(newArr) // ["one", "two", "three", "four"]
let arr = ['one', 'two', 'three'];
let newArr = arr.slice();
newArr.push('four')
console.log(arr) // ["one", "two", "three"]
console.log(newArr) // ["one", "two", "three", "four"]
let arr = [{a:1}, 'two', 'three'];
let newArr = arr.concat();
newArr[0].a = 2;
console.log(arr) // [{a: 2},"two","three"]
console.log(newArr) // [{a: 2},"two","three"]
複製代碼
Object assign()
該方法能夠把任意多個的源對象自身的可枚舉屬性拷貝給目標對象,而後返回目標對象。Object assign()
對對象的拷貝仍是淺拷貝
let arr = {
a: 'one',
b: 'two',
c: 'three'
};
let newArr = Object.assign({}, arr)
newArr.d = 'four'
console.log(arr); // {a: "one", b: "two", c: "three"}
console.log(newArr); // {a: "one", b: "two", c: "three", d: "four"}
let arr = {
a: 'one',
b: 'two',
c: {a: 1}
};
let newArr = Object.assign({}, arr)
newArr.c.a = 3;
console.log(arr); // {a: "one", b: "two", c: {a: 3}}
console.log(newArr); // {a: "one", b: "two", c: {a: 3}}
複製代碼
原理:遍歷對象,而後把屬性和屬性值放在一個新對象並返回
function clone(obj) {
// 只拷貝對象
if (typeof src !== 'object') return;
// 根據 obj 的類型判斷是新建一個數組仍是對象
let newObj = Obejct.prototype.toString.call(obj) == '[object Array]' ? [] : {};
for(let prop in newObj) {
if(newObj.hasOwnProperty(prop)) {
newObj[prop] = obj[src];
}
}
return newObj;
}
複製代碼
JSON.parse(JSON.stringify(arr))
:不只適用於數組還適用於對象
let a = {
name: "tn",
book: {
title: "JS",
price: "45"
}
}
let b = JSON.parse(JSON.stringify(a));
console.log(b);
// {
// name: "tn",
// book: {title: "JS", price: "45"}
// }
a.name = "change";
a.book.price = "55";
console.log(a);
// {
// name: "change",
// book: {title: "JS", price: "55"}
// }
console.log(b);
// {
// name: "tn",
// book: {title: "JS", price: "45"}
// }
複製代碼
改變變量 a 中的引用屬性後對 b 沒有任何影響,這就是深拷貝的魔力
對數組深拷貝以後,改變原數組頁不會影響到拷貝以後的數組
// 木易楊
let a = [0, "1", [2, 3]];
let b = JSON.parse(JSON.stringify( a.slice(1) ));
console.log(b); // ["1", [2, 3]]
a[1] = "99";
a[2][0] = 4;
console.log(a); // [0, "99", [4, 3]]
console.log(b); // ["1", [2, 3]]
複製代碼
但該方法有侷限性
undefined
symbol
new Date()
undefined
、symbol
和 函數
會被直接忽略
// 木易楊
let obj = {
name: "tn",
a: undefined,
b: Symbol("tn"),
c: function() {}
}
let b = JSON.parse(JSON.stringify(obj));
console.log(b); // {name: "tn"}
複製代碼
循環引用狀況下會報錯
let obj = {
a: 1,
b: {
c: 2,
d: 3
}
}
obj.a = obj.b;
obj.b.c = obj.a;
let b = JSON.parse(JSON.stringify(obj)); // Uncaught TypeError: Converting circular structure to JSON
複製代碼
new Date
狀況下轉換結果不正確
new Date(); // Mon Dec 24 2018 10:59:14 GMT+0800 (China Standard Time)
JSON.stringify(new Date()); // ""2018-12-24T02:59:25.776Z""
JSON.parse(JSON.stringify(new Date())); // "2018-12-24T02:59:41.523Z"
複製代碼
解決方法轉成字符串或者時間戳
let date = (new Date()).valueOf();
JSON.stringify(date); // "1625905818735"
JSON.parse(JSON.stringify(date)); // 1625905818735
複製代碼
正則狀況下
let obj = {
name: "tn",
a: /'123'/
}
console.log(obj); // {name: "tn", a: /'123'/}
let b = JSON.parse(JSON.stringify(obj));
console.log(b); // {name: "tn", a: {}}
複製代碼
ES6 擴展運算符[...]
:不只適用於數組還適用於對象,只有原始值能夠深拷貝,當含有引用值時進行淺拷貝
原理:在拷貝時判斷一下屬性值的類型,如果對象則遞歸調用深拷貝函數,深拷貝是徹底拷貝了原對象的內容並寄存在新的內存空間,指向新的內存地址
function deepClone1(src, target) {
var target = target || {};
for (let prop in src) {
if (src.hasOwnProperty(prop)) {
if(src[prop] !== null && typeof(src[prop]) === 'object') {
target[prop] = Object.prototype.toString.call(src[prop]) == '[object Array]' ? [] : {};
deepClone(src[prop], target[prop]);
} else {
target[prop] = src[prop];
}
}
}
return target;
}
// test
var a = {
name: "tn",
book: {
title: "JS",
price: "45"
},
a1: undefined,
a2: null,
a3: 123
}
var b = deepClone(a);
console.log(b);
// {
// a1: undefined,
// a2: null,
// a3: 123,
// book: {
// title: "JS",
// price: "45"
// },
// name: "tn"
// }
複製代碼
咱們知道 JSON
沒法深拷貝循環引用,遇到這種狀況會拋出異常
其實就是循環檢測,設置一個數組或哈希表存儲已拷貝過的對象,當檢測到當前對象已存在於哈希表中時,取出該值並返回
function deepClone2(src, hash = new WeakMap()) {
var target = Object.prototype.toString.call(src) == '[object Array]' ? [] : {};
if (hash.has(src)) return hash.get(src); // 新增代碼,查哈希表
hash.set(src, target); // 新增代碼,哈希表設值
for (let prop in src) {
if (src.hasOwnProperty(prop)) {
if(src[prop] !== null && typeof(src[prop]) === 'object') {
target[prop] = deepClone2(src[prop], hash);
} else {
target[prop] = src[prop];
}
}
}
return target;
}
var a = {
name: "tn",
book: {
title: "JS",
price: "45"
},
a1: undefined,
a2: null,
a3: 123
};
a.circleRef = a;
var b = deepClone2(a);
console.log(b);
// {
// a1: undefined,
// a2: null,
// a3: 123,
// book: {title: "JS", price: "45"},
// circleRef: {name: "tn", book: {…}, a1: undefined, a2: null, a3: 123, …},
// name: "tn"
// }
複製代碼
上面使用了 ES6
中的 WeakMap
來處理,在 ES5
下可使用數組來處理
function deepClone2(src, uniqueList) {
var target = Object.prototype.toString.call(src) == '[object Array]' ? [] : {};
if (!uniqueList) uniqueList = []; // 新增代碼,初始化數組
// 數據已經存在,返回保存的數據
var uniqueData = find(uniqueList, src);
if (uniqueData) {
return uniqueData.target;
};
// 數據不存在,保存源數據,以及對應的引用
uniqueList.push({
source: src,
target: target
});
for (let prop in src) {
if (src.hasOwnProperty(prop)) {
if(src[prop] !== null && typeof(src[prop]) === 'object') {
target[prop] = deepClone2(src[prop], uniqueList);
} else {
target[prop] = src[prop];
}
}
}
return target;
}
// 用上面用例測試 OK
複製代碼
如今已經很完美的解決了循環引用的狀況,但其實還有一種狀況是引用丟失,咱們看下面的例子
var obj1 = {};
var obj2 = {a: obj1, b: obj1};
obj2.a === obj2.b; // true
var obj3 = deepClone1(obj2);
obj3.a === obj3.b; // false
複製代碼
引用丟失在某些狀況下是有問題的,如上面的對象 obj2,obj2 的鍵值 a 和 b 同時引用了同一個對象 obj1,使用 deepClone1 進行深拷貝後就丟失了引用關係變成了兩個不一樣的對象
其實上面的 deepClone2 已經解決了這個問題,由於存儲了已拷貝過的對象
var obj3 = deepClone2(obj2);
obj3.a === obj3.b; // true
複製代碼
Symbol
在 ES6
下才有,須要一些方法來檢測出 Symble
類型
方法一:Object.getOwnPropertySymbols(...)
該方法能夠查找一個給定對象的符號屬性時返回一個 ?symbol 類型的數組。注意,每一個初始化的對象都是沒有本身的 symbol 屬性的,所以這個數組可能爲空,除非你已經在對象上設置了 symbol 屬性(來自MDN)
var obj = {};
var a = Symbol("a"); // 建立新的 symbol 類型
var b = Symbol.for("b"); // 從全局的 symbol 註冊?表設置和取得symbol
obj[a] = "localSymbol";
obj[b] = "globalSymbol";
var objectSymbols = Object.getOwnPropertySymbols(obj);
console.log(objectSymbols.length); // 2
console.log(objectSymbols) // [Symbol(a), Symbol(b)]
console.log(objectSymbols[0]) // Symbol(a)
複製代碼
思路就是先查找有沒有 Symbol
屬性,若是查找到則先遍歷處理 Symbol
狀況,而後再處理正常狀況
function deepClone3(src, hash = new WeakMap()) {
var target = Object.prototype.toString.call(src) == '[object Array]' ? [] : {};
if (hash.has(src)) return hash.get(src); // 新增代碼,查哈希表
hash.set(src, target); // 新增代碼,哈希表設值
let symKeys = Object.getOwnPropertySymbols(src); // 查找
if (symKeys.length) { // 查找成功
symKeys.forEach(symKey => {
if (src[prop] !== null && typeof(src[prop]) === 'object') {
target[symKey] = deepClone3(src[symKey], hash);
} else {
target[symKey] = src[symKey];
}
});
}
for (let prop in src) {
if (src.hasOwnProperty(prop)) {
if(src[prop] !== null && typeof(src[prop]) === 'object') {
target[prop] = deepClone3(src[prop], hash);
} else {
target[prop] = src[prop];
}
}
}
return target;
}
var a = {
name: "tn",
book: {
title: "JS",
price: "45"
},
a1: undefined,
a2: null,
a3: 123
};
var sym1 = Symbol("a"); // 建立新的symbol類型
var sym2 = Symbol.for("b"); // 從全局的symbol註冊?表設置和取得symbol
a[sym1] = "localSymbol";
a[sym2] = "globalSymbol";
var b = deepClone3(a);
console.log(b);
// {
// a1: undefined
// a2: null
// a3: 123,
// book: {title: "JS", price: "45"},
// circleRef: {name: "tn", book: {…}, a1: undefined, a2: null, a3: 123, …},
// name: "tn",
// [Symbol(a)]: "localSymbol",
// [Symbol(b)]: "globalSymbol"
// }
複製代碼
方法二:Reflect.ownKeys(...)
返回一個由目標對象自身的屬性鍵組成的數組。它的返回值等同於
Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target))
(來自MDN)
Reflect.ownKeys({z: 3, y: 2, x: 1}); // [ "z", "y", "x" ]
Reflect.ownKeys([]); // ["length"]
var sym = Symbol.for("comet");
var sym2 = Symbol.for("meteor");
var obj = {[sym]: 0, "str": 0, "773": 0, "0": 0,
[sym2]: 0, "-1": 0, "8": 0, "second str": 0};
Reflect.ownKeys(obj); // [ "0", "8", "773", "str", "-1", "second str", Symbol(comet), Symbol(meteor) ]
// 注意順序
// Indexes in numeric order,
// strings in insertion order,
// symbols in insertion order
複製代碼
function deepClone3(src, hash = new WeakMap()) {
var target = Object.prototype.toString.call(src) == '[object Array]' ? [] : {};
if (hash.has(src)) return hash.get(src);
hash.set(src, target);
Reflect.ownKeys(src).forEach(key => { // 改動
if (src[key] !== null && typeof(src[key]) === 'object') {
target[key] = deepClone3(src[key], hash);
} else {
target[key] = src[key];
}
});
return target;
}
// 測試 ok
複製代碼
這裏使用了 Reflect.ownKeys()
獲取全部的鍵值,同時包括 Symbol
,對 src
遍歷賦值便可
上面使用的都是遞歸方法,可是有個問題是可能會爆棧,錯誤提示以下
// RangeError: Maximum call stack size exceeded
複製代碼
詳情請參考這篇文章:深拷貝的終極探索(99%的人都不知道)
上面的方式能夠知足基本的場景的需求,如有更復雜的需求可本身實現。一些框架和庫的也有對應的解決方案,如:jQuery.extend()
、lodash
淺拷貝
對於一層結構的 Array
和 Object
想要拷貝一個副本時使用 vue
的 mixin
是淺拷貝的一種複雜型式
深拷貝
複製深層次的 object
數據結構,如想對某個數組或對象的值進行修改,但又要保留原數組或對象的值不被修改,此時就能夠用深拷貝來建立一個新的數組或對象
javascript中的深拷貝和淺拷貝?
JavaScript 如何完整實現深度Clone對象?
ithub lodash源碼
MDN 結構化克隆算法 jQuery v3.2.1 源碼