拷貝
復制與拷貝
let user = {
name: "John"
};
let user2=user; //變量名復制,只是持有了源對象的引用
let userClone=clone(user);//對象克隆,新對象是是源對象的拷貝
復制:將一個對象a賦值給另一個變量b,這個只是存儲了對象a的引用地址,是屬于同一個對象
克隆:創建一份獨立的對象拷貝,新對象具有源對象項的所有可枚舉屬性(值),兩個對象之間相互獨立
淺拷貝
思路:聲明一個新對象,將源對象的可枚舉屬性(值)拷貝到新對象上
實現方式
-
for...in
復制所有屬性值
- 會拷貝對象自身以及其原型鏈上的可枚舉屬性
let dest = {}; // 新的空對象
// 復制所有的屬性值
for (let key in src) {
dest[key] = src[key];
}
- 采用jQuery使用extend,
jQuery.extent(dest,src)
以默認配置為優先,用戶設置為覆蓋
賦值對象的可枚舉屬性
- 會拷貝對象自身以及其原型鏈上的可枚舉屬性
- 無法處理值為undefined的屬性/值
- 只拷貝對象中基本數據類型的屬性,對于引用數據類型的數據會保持對象引用,
-
Object.assign(dest,[ src1, src2, src3...])
,將 src1, ..., srcN 這些所有的對象復制到 dest
- 只拷貝對象中基本數據類型的屬性,對于引用數據類型的數據會保持對象引用
- 如果目標對象中的屬性具有相同的鍵,則屬性將被源對象中的屬性覆蓋。后面的源對象的屬性將類似地覆蓋前面的源對象的屬性。
- 只會拷貝源對象自身可枚舉的屬性到目標對象。該方法使用源對象的
[[Get]]
和目標對象的[[Set]]
,所以它會調用相關 getter 和 setter。因此,它分配屬性,而不僅僅是復制或定義新的屬性。如果合并源包含getter,這可能使其不適合將新屬性合并到原型中。為了將屬性定義(包括其可枚舉性)復制到原型,應使用Object.getOwnPropertyDescriptor()和Object.defineProperty() 。- String類型和 Symbol 類型的屬性都會被拷貝。
- 在出現錯誤的情況下,例如,如果屬性不可寫,會引發TypeError,如果在引發錯誤之前添加了任何屬性,則可以更改target對象。
- 不會在那些src對象值為 null或 undefined 的時候拋出錯誤。
- 原始類型會被包裝為對象
總結
無法正常處理屬性(值)為引用類型的數據,
深拷貝
思路:復制的時候應該檢查
obj[key]
的每一個值,如果它是一個對象,那么把它也復制一遍
實現方式
jQuery.extend(true,dest,src)
,會遞歸處理對象的中引用數據類型屬性(值)JSON.parse(JSON.stringify(obj))
- 無法拷貝對象中
Function類型
的屬性 - 無法拷貝對象中值為undefined的屬性
- 無法拷貝具有循環引用的對象(可用來檢測對象是否循環引用)
- 基于遞歸實現
var deepClone=function(obj) {
// 處理數組
if(isArray(obj)){
return obj.map(function(ele) {
return isArray(ele)||isObject(ele)?deepClone(ele):ele
})
} else if(isObject(obj)){
return reduce(obj,function(memo,value,key) {
memo[key]=isArray(value)||isObject(value)?deepClone(value):value
return memo
},{})
}else {
return obj
}
}
以上版本并未處理循環引用問題,以及特殊的引用數據類型(Set/Map/RegExp等)
循環引用
我們先來看個例子
var man = {
name: 'amsterdam',
sex: 'male'
};
man['father'] = man;
對象man
的屬性father
又指向了man
本身,形成了“環”,如果不能正常處理此類情況,將出現調用棧溢出。
有一個標準的深拷貝算法,用于解決上面這種和一些更復雜的情況,叫做 結構化克隆算法(Structured cloning algorithm)。
算法的優點是:
- 可以復制 RegExp 對象。
- 可以復制 Blob、File 以及 FileList 對象。
- 可以復制 ImageData 對象。CanvasPixelArray 的克隆粒度將會跟原始對象相同,并且復制出來相同的像素數據。
- 可以正確的復制有循環引用的對象
依然存在的缺陷是:
Error 以及 Function 對象是不能被結構化克隆算法復制的;如果你嘗試這樣子去做,這會導致拋出 DATA_CLONE_ERR 的異常。
企圖去克隆 DOM 節點同樣會拋出 DATA_CLONE_ERROR 異常。
-
對象的某些特定參數也不會被保留
- RegExp 對象的 lastIndex 字段不會被保留
- 屬性描述符,setters 以及 getters(以及其他類似元數據的功能)同樣不會被復制。例如,如果一個對象用屬性描述符標記為 read-only,它將會被復制為 read-write,因為這是默認的情況下。
- 原形鏈上的屬性也不會被追蹤以及復制。
可參考lodash等庫函數的實現
手動實現深拷貝
const deepCloneClourse = (target) => {
let cached = new WeakMap()
function baseClone (obj) {
let objectType = getType(obj)
let cloneObj
// 檢測對象是否已克隆 返回克隆后的對象
let temp = cache(cached, obj)
if (temp) {
return temp
}
switch (objectType) {
// Object
case 'Object':
//緩存已克隆對象
cached.set(obj, cloneObj = {})
//key-value 類型中Key可能是symbol
Object.getOwnPropertySymbols(obj).forEach(item => {
let symbol = Object(Symbol.prototype.valueOf.call(item))
cloneObj[symbol] = baseClone(obj[item])
})
break
// 容器類
case 'Set':
//緩存已克隆對象
cached.set(obj, cloneObj = new Set())
obj.forEach((val) => {
cloneObj.add(baseClone(val, cached))
})
break
case 'Map':
//緩存已克隆對象
cached.set(obj, cloneObj = new Map())
obj.forEach((val, key) => {
cloneObj.set(key, baseClone(val))
})
//key-value 類型中Key可能是symbol
Object.getOwnPropertySymbols(obj).forEach(item => {
let symbol = Object(Symbol.prototype.valueOf.call(item))
cloneObj[symbol] = baseClone(obj[item])
})
break
case 'Array':
//緩存已克隆對象
cached.set(obj, cloneObj = [])
obj.forEach((val) => {
cloneObj.push(baseClone(val))
})
break
// 普通對象
case 'RegExp':
cloneObj = new RegExp(obj.source, obj.flags)
break
case 'Date':
cloneObj = new Date(obj)
break
case 'Symbol':
cloneObj = Object(Symbol.prototype.valueOf.call(obj))
break
case 'Boolean':
cloneObj = Boolean(obj)
break
case 'Function':
cloneObj = function () {
return obj.apply(this, arguments)
}
break
default://null undefined NaN string number boolean
cloneObj = obj
}
if (typeof obj === 'object') {
for (let item in obj) {
if (obj.hasOwnProperty(item)) {
cloneObj[item] = baseClone(obj[item])
}
}
}
return cloneObj
}
return baseClone(target)
}
總結
- 在實際開發過程中,我們可以預估對象的基本結構,正確的使用深淺拷貝,避免在函數中因修改對象值照成數據異常的情形。
- 大而全的東西,往往是最昂貴的。