原文:你真的會克隆物件嗎
開始之前
在開始聊克隆之前,我們還是先來看看js資料型別。js的資料型別分為基本資料型別和複雜資料型別。
- 基本資料型別:Number、Boolean、String、Null、String、Symbol(ES6 新增)
- 複雜資料型別:Object,其他引用型別(Array、Date、RegExp、Function、基本包裝型別(Boolean、String、Number)、Math等)都是Object型別的例項物件
克隆:基本資料 => 複製這個變數;複雜資料 => 拷貝引用(網上的介紹很多,不深入了)
常見淺拷貝
對於物件的克隆,應該大多數人都能實現出來,可能深、淺拷貝都能想出好幾種方式,我們先來聊聊淺拷貝。
一個常見的淺拷貝一般是下面這樣:
function shallowCopy (obj) {
if (typeof obj !== 'object') {
return
}
var newObj = obj instanceof Array ? [] : {}
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
newObj[key] = obj[key]
}
}
return newObj
}
複製程式碼
或者更嚴謹一點的實現陣列的判斷:
Object.prototype.toString.call(arr) === '[object Array]'
複製程式碼
好像是沒什麼問題呢,畢竟經過了好多專案的檢測,網上一搜就能出現一大堆。
但是,我們開頭介紹資料型別的時候就已經說過了,ES6新增了Symbol
型別,情況好像就有點不一樣了
Symbol
Symbol
是ES6中引入的原始資料型別。Symbol
值通過Symbol
函式生成,是獨一無二的。同時,ES6中規定了物件的屬性名有兩種型別,一種是字串,另一種就是 Symbol
型別。凡是屬性名屬於 Symbol 型別,就不會與其他屬性名產生衝突。但是,隨之而來的問題是,我們的for...in
迴圈不能遍歷出該屬性
Symbol
作為屬性名,該屬性不會出現在for...in
、for...of
迴圈中,也不會被Object.keys()
、Object.getOwnPropertyNames()
、JSON.stringify()
返回。但是,它也不是私有屬性,有一個Object.getOwnPropertySymbols
方法,可以獲取指定物件的所有 Symbol 屬性名。
有Symbol
型別,自然有遍歷Symbol
型別的方法。Object.getOwnPropertySymbols
+ for...in
的組合起來好像是能滿足我們要求的了。嗯,看起來還不錯,但是似乎有點麻煩了,有沒有更便捷一點的方式呢?或許新時代的男人---Reflect.ownKeys
,要閃亮登場了,這個既能遍歷字串,又能遍歷Symbol
的死變態(請允許我這麼誇他)。
Reflect.ownKeys
返回一個陣列,包含物件自身的所有屬性,不管是屬性名是Symbol
或字串,也不管是否可列舉
Object.assign
這個時候熟悉ES6的人或許開始有疑問了,我們已經開始討論Symbol
和Reflect.ownKeys
,為什麼淺克隆不直接用Object.assign
或者展開運算子(...
)呢?
嗯,待我吃根火腿冷靜冷靜,好像你說的很對!Object.assign
的確是能拷貝Symbol
型別的呢。但是呢,但是呢,我們是一個有追求的猿類,多一種實現方式不是能讓我們多瞭解一些坑嗎?而且這種方式不是能讓我們更靈活的實現不可預知的需求嗎?對,沒錯,是這樣子的...
Object.assign
這個更完美的男人出來之後,好像淺拷貝部分也該結束了,正常來說,的確是這樣。不過我們再仔細想想上面的兩種方式,好像還是有點區別的呢。我們再來看看這兩個男人:
Reflect.ownKeys
返回一個陣列,包含物件自身的所有屬性,不管是屬性名是Symbol或字串,也不管是否可列舉Object.assign
拷貝的屬性是有限制的,只拷貝源物件的自身屬性(不拷貝繼承屬性),也不拷貝不可列舉的屬性
注意到了嗎?這裡面有一個是否可列舉的概念,這個時候是不是應該感慨我們知道怎麼實現不可預知的需求了呢。
不可列舉
我們先看個例子:
var obj = Object.create({ foo: 1 }, {
[Symbol()]: {
value: 1,
enumerable: false
},
bar: {
value: 2,
enumerable: false
},
[Symbol()]: {
value: 3,
enumerable: true
},
baz: {
value: 4,
enumerable: true
}
})
Object.assign({}, obj) // {baz: 4, Symbol(): 3}
複製程式碼
唉,的確是這樣呢!看來Object.assign
也不是我們的理想歸宿啊。我們再回過頭來看看Reflect.ownKeys
,上面挖的坑也該填了,我們在講Symbol
的時候,Object.getOwnPropertySymbols
+ for...in
直接用Reflect.ownKeys
替代了,在從可列舉的角度出發看看,好像哪裡不對,for...in
只能迴圈遍歷物件自身的和繼承的可列舉的屬性,且不含 Symbol
。頭都大了嗎?來來來,喝完這杯,還有一杯,繼續接著來。這麼多迴圈,我們來縷縷頭緒:
for...in
迴圈遍歷物件自身的和繼承的可列舉屬性(不含Symbol
屬性)。Object.keys()
返回一個陣列,包括物件自身的(不含繼承的)所有可列舉屬性(不含Symbol
屬性)的鍵名。Object.getOwnPropertyNames()
返回一個陣列,包含物件自身的所有屬性(不含Symbol
屬性,但是包括不可列舉屬性)的鍵名。Object.getOwnPropertySymbols()
返回一個陣列,包含物件自身的所有Symbol
屬性的鍵名。Reflect.ownKeys()
返回一個陣列,包含物件自身的所有鍵名,不管鍵名是Symbol
或字串,也不管是否可列舉。
終於清晰了,或許也該結束了吧。
慢著,好像上面的例子讓我想到了什麼!!!
屬性描述符
我們在來思考一個例子:
const source = {
get foo() { return 1 }
};
const target = {};
Object.assign(target, source) // { foo: 1 }
複製程式碼
好像並不是我們想要的呢,遍歷的方式好像也不適用了,這可怎麼辦。別急,還有Object.getOwnPropertyDescriptors
可以用。
ES2017 引入了
Object.getOwnPropertyDescriptors
方法,返回指定物件所有自身屬性(非繼承屬性)的描述物件
仔細閱讀下文件,終於用Object.getOwnPropertyDescriptors
+Object.getPrototypeOf
成功了呢
Object.create(
Object.getPrototypeOf(obj),
Object.getOwnPropertyDescriptors(obj)
)
複製程式碼
寫到這裡,淺拷貝部分也該結束了
結束語
可能實際專案中並不需要處理的這麼細緻,但是希望大家對各種遍歷、實現一個淺拷貝以及ES6的一些知識有一個總結和一點新的認識吧,本來想繼續寫深拷貝的,無賴篇幅已經不短,加上長夜漫漫,我想睡覺,深拷貝的問題更復雜,我先放放,日後再說。
最後的最後,作為一個前端界的小學生,第一次用掘金寫文章,雖然使用掘金挺久了,發表文章還是有點慌,文章有什麼錯誤之處,還請指正。同時對這篇文章有興趣的朋友,可以繼續關注下一篇的深克隆,會更新的會更新的...