此前寫過一篇文章:JavaScript深淺拷貝氯哮,其實沒那么難老速!贞绵,但里面的拷貝處理顯然不夠理想者祖。
今天再來詳細(xì)的講講...
一、JSON.stringify() 的缺陷
利用 JavaScript 內(nèi)置的 JSON 處理函數(shù)屁商,可以實現(xiàn)簡易的深拷貝:
const obj = {
// ...
}
JSON.parse(JSON.stringify(obj)) // 序列化與反序列化
這個方法烟很,其實能適用于 90% 以上的應(yīng)用場景。畢竟多數(shù)項目下蜡镶,很少會去拷貝一個函數(shù)什么的雾袱。
但不得不說,這里面有“坑”官还,這些“坑”是 JSON.stringify()
方法本身實現(xiàn)邏輯產(chǎn)生的:
JSON.stringify(value[, replacer[, space]])
該方法有以下特點:
- 布爾值芹橡、數(shù)值、字符串對應(yīng)的包裝對象望伦,在序列化過程會自動轉(zhuǎn)換成其原始值僻族。
-
undefined
、任意函數(shù)
屡谐、Symbol 值
,在序列化過程有兩種不同的情況蝌数。若出現(xiàn)在非數(shù)組對象的屬性值中愕掏,會被忽略;若出現(xiàn)在數(shù)組中顶伞,會轉(zhuǎn)換成null
饵撑。 -
任意函數(shù)
、undefined
被單獨轉(zhuǎn)換時唆貌,會返回undefined
滑潘。 - 所有
以 Symbol 為屬性鍵的屬性
都會被完全忽略,即便在該方法第二個參數(shù)replacer
中指定了該屬性锨咙。 -
Date 日期
調(diào)用了其內(nèi)置的toJSON()
方法轉(zhuǎn)換成字符串语卤,因此會被當(dāng)初字符串處理。 -
NaN
和Infinity
的數(shù)值及null
都會當(dāng)做null
酪刀。 - 這些對象
Map
粹舵、Set
、WeakMap
骂倘、WeakSet
僅會序列化可枚舉的屬性眼滤。 - 被轉(zhuǎn)換值如果含有
toJSON()
方法,該方法定義什么值將被序列化历涝。 - 對包含
循環(huán)引用
的對象進(jìn)行序列化诅需,會拋出錯誤漾唉。
二、深拷貝的邊界
其實堰塌,針對以上兩個內(nèi)置的全局方法赵刑,還有這么多情況不能處理,是不是很氣人蔫仙。其實不然料睛,我猜測 JSON.parse()
和 JSON.stringify()
只是讓我們更方便地操作符合 JSON 格式的 JavaScript 對象或符合 JSON 格式的字符串。
至于上面提到的“坑”摇邦,很明顯是不符合作為跨平臺數(shù)據(jù)交換的格式要求的恤煞。在 JSON 中,它有 null
施籍,是沒有 undefined
居扒、Symbol
類型、函數(shù)等丑慎。
JSON 是一種數(shù)據(jù)格式喜喂,也可以說是一種規(guī)范。JSON 是用于跨平臺數(shù)據(jù)交流的竿裂,獨立于語言和平臺玉吁。而 JavaScript 對象是一個實例,存在于內(nèi)存中腻异。JavaScript 對象是沒辦法傳輸?shù)慕保挥性诒恍蛄谢癁?JSON 字符串后才能傳輸。
此前寫過一篇文章悔常,介紹了 JSON 和 JavaScript 的關(guān)系以及上述兩個方法的一些細(xì)節(jié)影斑。可看:詳談 JSON 與 JavaScript机打。
如果自己實現(xiàn)一個深拷貝的方法矫户,其實是有很多邊界問題要處理的,至于這些種種的邊界 Case残邀,要不要處理最好從實際情況出發(fā)皆辽。
常見的邊界 Case 有什么呢?
主要有循環(huán)引用芥挣、包裝對象膳汪、函數(shù)、原型鏈九秀、不可枚舉屬性遗嗽、Map/WeakMap、Set/WeakSet鼓蜒、RegExp痹换、Symbol征字、Date、ArrayBuffer娇豫、原生 DOM/BOM 對象等匙姜。
就目前而言,第三方最完善的深拷貝方法是 Lodash 庫的 _.cloneDeep()
方法了冯痢。在實際項目中氮昧,如需處理 JSON.stringify()
無法解決的 Case,我會推薦使用它浦楣。否則請使用內(nèi)置 JSON 方法即可袖肥,沒必要復(fù)雜化。
但如果為了學(xué)習(xí)深拷貝振劳,那應(yīng)該要每種情況都要去嘗試實現(xiàn)一下椎组,我想這也是你在看這篇文章的原意。這樣历恐,無論是實現(xiàn)特殊要求的深拷貝寸癌,還是面試,都可以從容應(yīng)對弱贼。
下面一起來學(xué)習(xí)吧蒸苇,如有不足,歡迎指出 ?? ~
三吮旅、自實現(xiàn)深拷貝方法
主要運用到遞歸的思路去實現(xiàn)一個深拷貝方法溪烤。
PS:完整的深拷貝方法會在文章最后放出。
先寫一個簡易版本:
const deepCopy = source => {
// 判斷是否為數(shù)組
const isArray = arr => Object.prototype.toString.call(arr) === '[object Array]'
// 判斷是否為引用類型
const isObject = obj => obj !== null && (typeof obj === 'object' || typeof obj === 'function')
// 拷貝(遞歸思路)
const copy = input => {
if (typeof input === 'function' || !isObject(input)) return input
const output = isArray(input) ? [] : {}
for (let key in input) {
if (input.hasOwnProperty(key)) {
const value = input[key]
output[key] = copy(value)
}
}
return output
}
return copy(source)
}
以上簡易版本還存在很多情況要特殊處理鸟辅,接下來針對 JSON.stringify()
的缺陷,一點一點去完善它莺葫。
3.1 針對布爾值匪凉、數(shù)值、字符串的包裝對象的處理
需要注意的是捺檬,從 ES6 開始圍繞原始數(shù)據(jù)類型創(chuàng)建一個顯式包裝器對象不再被支持再层。但由于遺留原因,現(xiàn)有的原始包裝器對象(如
new Boolean
堡纬、new Number
聂受、new String
)仍可使用。這也是 ES6+ 新增的Symbol
烤镐、BigInt
數(shù)據(jù)類型無法通過new
關(guān)鍵字創(chuàng)建實例對象的原因蛋济。
由于 for...in 無法遍歷不可枚舉的屬性。例如炮叶,包裝對象的 [[PrimitiveValue]]
內(nèi)部屬性碗旅,因此需要我們特殊處理一下渡处。
以上結(jié)果,顯然不是預(yù)期結(jié)果祟辟。包裝對象的 [[PrimitiveValue]]
屬性可通過 valueOf()
方法獲取医瘫。
const deepCopy = source => {
// 獲取數(shù)據(jù)類型(本次新增)
const getClass = x => Object.prototype.toString.call(x)
// 判斷是否為數(shù)組
const isArray = arr => getClass(arr) === '[object Array]'
// 判斷是否為引用類型
const isObject = obj => obj !== null && (typeof obj === 'object' || typeof obj === 'function')
// 判斷是否為包裝對象(本次新增)
const isWrapperObject = obj => {
const theClass = getClass(obj)
const type = /^\[object (.*)\]$/.exec(theClass)[1]
return ['Boolean', 'Number', 'String', 'Symbol', 'BigInt'].includes(type)
}
// 處理包裝對象(本次新增)
const handleWrapperObject = obj => {
const type = getClass(obj)
switch (type) {
case '[object Boolean]':
return Object(Boolean.prototype.valueOf.call(obj))
case '[object Number]':
return Object(Number.prototype.valueOf.call(obj))
case '[object String]':
return Object(String.prototype.valueOf.call(obj))
case '[object Symbol]':
return Object(Symbol.prototype.valueOf.call(obj))
case '[object BigInt]':
return Object(BigInt.prototype.valueOf.call(obj))
default:
return undefined
}
}
// 拷貝(遞歸思路)
const copy = input => {
if (typeof input === 'function' || !isObject(input)) return input
// 處理包裝對象(本次新增)
if (isWrapperObject(input)) {
return handleWrapperObject(input)
}
// 其余部分沒變,為了減少篇幅旧困,省略一萬字...
}
return copy(source)
}
我們在控制臺打印一下結(jié)果醇份,可以看到是符合預(yù)期結(jié)果的。
3.2 針對函數(shù)的處理
直接返回就好了吼具,一般不用處理僚纷。在實際應(yīng)用場景需要拷貝函數(shù)太少了...
const copy = input => {
if (typeof input === 'function' || !isObject(input)) return input
}
3.3 針對以 Symbol 值作為屬性鍵的處理
由于以上 for...in
方法無法遍歷 Symbol
的屬性鍵,因此:
const sym = Symbol('desc')
const obj = {
[sym]: 'This is symbol value'
}
console.log(deepCopy(obj)) // {}馍悟,拷貝結(jié)果沒有 [sym] 屬性
這里畔濒,我們需要用到兩個方法:
Object.getOwnPropertySymbols()
它返回一個對象自身的所有 Symbol 屬性的數(shù)組,包括不可枚舉的屬性锣咒。Object.prototype.propertyIsEnumerable()
它返回一個布爾值侵状,表示指定的屬性是否可枚舉。
const copy = input => {
// 其它不變
for (let key in input) {
// ...
}
// 處理以 Symbol 值作為屬性鍵的屬性(本次新增)
const symbolArr = Object.getOwnPropertySymbols(input)
if (symbolArr.length) {
for (let i = 0, len = symbolArr.length; i < len; i++) {
if (input.propertyIsEnumerable(symbolArr[i])) {
const value = input[symbolArr[i]]
output[symbolArr[i]] = copy(value)
}
}
}
// ...
}
下面我們對 source
對象做拷貝操作:
const source = {}
const sym1 = Symbol('1')
const sym2 = Symbol('2')
Object.defineProperties(source,
{
[sym1]: {
value: 'This is symbol value.',
enumerable: true
},
[sym2]: {
value: 'This is a non-enumerable property.',
enumerable: false
}
}
)
打印結(jié)果毅整,也符合預(yù)期結(jié)果:
3.4 針對 Date 對象的處理
其實趣兄,處理 Date
對象,跟上面提到的包裝對象的處理是差不多的悼嫉。暫時先放到 isWrapperObject()
和 handleWrapperObject()
中處理艇潭。
const deepCopy = source => {
// 其他不變...
// 判斷是否為包裝對象(本次更新)
const isWrapperObject = obj => {
const theClass = getClass(obj)
const type = /^\[object (.*)\]$/.exec(theClass)[1]
return ['Boolean', 'Number', 'String', 'Symbol', 'BigInt', 'Date'].includes(type)
}
// 處理包裝對象
const handleWrapperObject = obj => {
const type = getClass(obj)
switch (type) {
// 其他 case 不變
// ...
case '[object Date]':
return new Date(obj.valueOf()) // new Date(+obj)
default:
return undefined
}
}
// 其他不變...
}
3.5 針對 Map、Set 對象的處理
同樣的戏蔑,暫時先放到 isWrapperObject()
和 handleWrapperObject()
中處理蹋凝。
利用 Map、Set 對象的 Iterator 特性和自身的方法总棵,可以快速解決鳍寂。
const deepCopy = source => {
// 其他不變...
// 判斷是否為包裝對象(本次更新)
const isWrapperObject = obj => {
const theClass = getClass(obj)
const type = /^\[object (.*)\]$/.exec(theClass)[1]
return ['Boolean', 'Number', 'String', 'Symbol', 'BigInt', 'Date', 'Map', 'Set'].includes(type)
}
// 處理包裝對象
const handleWrapperObject = obj => {
const type = getClass(obj)
switch (type) {
// 其他 case 不變
// ...
case '[object Map]': {
const map = new Map()
obj.forEach((item, key) => {
// 需要注意的是,這里的 key 不能深拷貝情龄,否則就會失去引用了
// 具體原因可以思考一下迄汛,不難。想不明白再評論區(qū)吧
map.set(key, copy(item))
})
return map
}
case '[object Set]': {
const set = new Set()
obj.forEach(item => {
set.add(copy(item))
})
return set
}
default:
return undefined
}
}
// 其他不變...
}
打印下結(jié)果:
3.6 針對循環(huán)引用的問題
以下是一個循環(huán)引用(circular reference)的對象:
const foo = { name: 'Frankie' }
foo.bar = foo
上面提到 JSON.stringify()
無法處理循環(huán)引用的問題骤视,我們在控制臺打印一下:
從結(jié)果可以看到鞍爱,當(dāng)對循環(huán)引用的對象進(jìn)行序列化處理時,會拋出類型錯誤:Uncaught TypeError: Converting circular structure to JSON
专酗。
接著睹逃,使用自行實現(xiàn)的 deepCopy()
方法,看下結(jié)果是什么:
我們看到祷肯,在拷貝循環(huán)引用的 foo
對象時唯卖,發(fā)生棧溢出了粱玲。
在另一篇文章,我提到過使用 JSON-js 可以處理循環(huán)引用的問題拜轨,具體用法是抽减,先引入其中的
cycle.js
腳本,然后JSON.stringify(JSON.decycle(foo))
就 OK 了橄碾。但究其根本卵沉,它使用了 WeakMap 去處理。
那我們?nèi)崿F(xiàn)一下:
const deepCopy = source => {
// 創(chuàng)建一個 WeakMap 對象法牲,記錄已拷貝過的對象(本次新增)
const weakmap = new WeakMap()
// 中間這塊不變史汗,省略一萬字...
// 拷貝(遞歸思路)
const copy = input => {
if (typeof input === 'function' || !isObject(input)) return input
// 針對已拷貝過的對象,直接返回(本次新增拒垃,以解決循環(huán)引用的問題)
if (weakmap.has(input)) {
return weakmap.get(input)
}
// 處理包裝對象
if (isWrapperObject(input)) {
return handleWrapperObject(input)
}
const output = isArray(input) ? [] : {}
// 記錄每次拷貝的對象
weakmap.set(input, output)
for (let key in input) {
if (input.hasOwnProperty(key)) {
const value = input[key]
output[key] = copy(value)
}
}
// 處理以 Symbol 值作為屬性鍵的屬性
const symbolArr = Object.getOwnPropertySymbols(input)
if (symbolArr.length) {
for (let i = 0, len = symbolArr.length; i < len; i++) {
if (input.propertyIsEnumerable(symbolArr[i])) {
output[symbolArr[i]] = input[symbolArr[i]]
}
}
}
return output
}
return copy(source)
}
先看看打印結(jié)果停撞,不會像之前一樣溢出了。
需要注意的是悼瓮,這里不使用 Map 而是 WeakMap 的原因:
首先戈毒,Map 的鍵屬于強(qiáng)引用,而 WeakMap 的鍵則屬于弱引用横堡。且 WeakMap 的鍵必須是對象埋市,WeakMap 的值則是任意的。
由于它們的鍵與值的引用關(guān)系命贴,決定了 Map 不能確保其引用的對象不會被垃圾回收器回收的引用道宅。假設(shè)我們使用的 Map,那么圖中的 foo
對象和我們深拷貝內(nèi)部的 const map = new Map()
創(chuàng)建的 map
對象一直都是強(qiáng)引用關(guān)系胸蛛,那么在程序結(jié)束之前污茵,foo
不會被回收,其占用的內(nèi)存空間一直不會被釋放葬项。
相比之下泞当,原生的 WeakMap 持有的是每個鍵對象的“弱引用”,這意味著在沒有其他引用存在時垃圾回收能正確進(jìn)行玷室。原生 WeakMap 的結(jié)構(gòu)是特殊且有效的零蓉,其用于映射的 key 只有在其沒有被回收時才是有效的笤受。
基本上穷缤,如果你要往對象上添加數(shù)據(jù),又不想干擾垃圾回收機(jī)制箩兽,就可以使用 WeakMap津肛。
可看 Why WeakMap?
我們熟知的 Lodash 庫的深拷貝方法,自實現(xiàn)了一個類似 WeakMap 特性的構(gòu)造函數(shù)去處理循環(huán)引用的汗贫。(詳看)
這里提供另一個思路身坐,也是可以的秸脱。
const deepCopy = source => {
// 其他一樣,省略一萬字...
// 創(chuàng)建一個數(shù)組部蛇,將每次拷貝的對象放進(jìn)去
const copiedArr = []
// 拷貝(遞歸思路)
const copy = input => {
if (typeof input === 'function' || !isObject(input)) return input
// 循環(huán)遍歷摊唇,若有已拷貝過的對象,則直接放回涯鲁,以解決循環(huán)引用的問題
for (let i = 0, len = copiedArr.length; i < len; i++) {
if (input === copiedArr[i].key) return copiedArr[i].value
}
// 處理包裝對象
if (isWrapperObject(input)) {
return handleWrapperObject(input)
}
const output = isArray(input) ? [] : {}
// 記錄每一次的對象
copiedArr.push({ key: input, value: output })
// 后面的流程不變...
}
return copy(source)
}
此前實現(xiàn)有個 bug巷查,感謝蝦蝦米指出,現(xiàn)已更正抹腿。
請在實現(xiàn)深拷貝之后測試以下示例:
const foo = { name: 'Frankie' }
foo.bar = foo
const cloneObj = deepCopy(foo) // 自實現(xiàn)深拷貝
const lodashObj = _.cloneDeep(foo) // Lodash 深拷貝
// 打印結(jié)果如下岛请,說明是正確的
console.log(lodashObj.bar === lodashObj) // true
console.log(lodashObj.bar === foo) // false
console.log(cloneObj.bar === cloneObj) // true
console.log(cloneObj.bar === foo) // false
3.7 針對正則表達(dá)式的處理
正則表達(dá)式里面,有兩個非常重要的屬性:
-
RegExp.prototype.source
返回當(dāng)前正則表達(dá)式對象的模式文本的字符串警绩。注意崇败,這是 ES6 新增的屬性。 -
RegExp.prototype.flags
返回當(dāng)前正則表達(dá)式對象標(biāo)志肩祥。
const { source, flags } = /\d/g
console.log(source) // "\\d"
console.log(flags) // "g"
有了以上兩個屬性后室,我們就可以使用 new RegExp(pattern, flags)
構(gòu)造函數(shù)去創(chuàng)建一個正則表達(dá)式了。
const { source, flags } = /\d/g
const newRegex = new RegExp(source, flags) // /\d/g
但需要注意的是搭幻,正則表達(dá)式有一個 lastIndex
屬性咧擂,該屬性可讀可寫,其值為整型檀蹋,用來指定下一次匹配的起始索引松申。在設(shè)置了 global
或 sticky
標(biāo)志位的情況下(如 /foo/g
、/foo/y
)俯逾,JavaScript RegExp
對象是有狀態(tài)的贸桶。他們會將上次成功匹配后的位置記錄在 lastIndex
屬性中。
因此桌肴,上述拷貝正則表達(dá)式的方式是有缺陷的皇筛。看示例:
const re1 = /foo*/g
const str = 'table football, foosball'
let arr
while ((arr = re1.exec(str)) !== null) {
console.log(`Found ${arr[0]}. Next starts at ${re1.lastIndex}.`)
}
// 以上語句會輸出坠七,以下結(jié)果:
// "Found foo. Next starts at 9."
// "Found foo. Next starts at 19."
// 當(dāng)我們修改 re1 的 lastIndex 屬性時水醋,輸出以下結(jié)果:
re1.lastIndex = 9
while ((arr = re1.exec(str)) !== null) {
console.log(`Found ${arr[0]}. Next starts at ${re1.lastIndex}.`)
}
// "Found foo. Next starts at 19."
// 以上這些相信你們都都懂。
所以彪置,你可以發(fā)現(xiàn)以下示例拄踪,打印結(jié)果是不一致的,原因就是使用 RegExp
構(gòu)造函數(shù)去創(chuàng)建一個正則表達(dá)式時拳魁,lastIndex
會默認(rèn)設(shè)為 0
惶桐。
const re1 = /foo*/g
const str = 'table football, foosball'
let arr
// 修改 lastIndex 屬性
re1.lastIndex = 9
// 基于 re1 拷貝一個正則表達(dá)式
const re2 = new RegExp(re1.source, re1.flags)
console.log('re1:')
while ((arr = re1.exec(str)) !== null) {
console.log(`Found ${arr[0]}. Next starts at ${re1.lastIndex}.`)
}
console.log('re2:')
while ((arr = re2.exec(str)) !== null) {
console.log(`Found ${arr[0]}. Next starts at ${re2.lastIndex}.`)
}
// re1:
// expected output: "Found foo. Next starts at 19."
// re2:
// expected output: "Found foo. Next starts at 9."
// expected output: "Found foo. Next starts at 19."
因此:
const deepCopy = source => {
// 其他不變,省略...
// 處理正則表達(dá)式
const handleRegExp = regex => {
const { source, flags, lastIndex } = regex
const re = new RegExp(source, flags)
re.lastIndex = lastIndex
return re
}
// 拷貝(遞歸思路)
const copy = input => {
if (typeof input === 'function' || !isObject(input)) return input
// 正則表達(dá)式
if (getClass(input) === '[object RegExp]') {
return handleRegExp(input)
}
// 后面不變,省略...
}
return copy(source)
}
打印結(jié)果也是符合預(yù)期的:
由于 RegExp.prototype.flags
是 ES6 新增屬性姚糊,我們可以看下 ES5 是如何實現(xiàn)的(源自 Lodash):
/** Used to match `RegExp` flags from their coerced string values. */
var reFlags = /\w*$/;
/**
* Creates a clone of `regexp`.
*
* @private
* @param {Object} regexp The regexp to clone.
* @returns {Object} Returns the cloned regexp.
*/
function cloneRegExp(regexp) {
var result = new regexp.constructor(regexp.source, reFlags.exec(regexp));
result.lastIndex = regexp.lastIndex;
return result;
}
但還是那句話贿衍,都 2021 年了,兼容 ES5 的問題就放心交給 Babel 吧救恨。
3.8 處理原型
注意贸辈,這里只實現(xiàn)類型為
"[object Object]"
的對象的原型拷貝。例如數(shù)組等不處理肠槽,因為這些情況實際場景太少了裙椭。
主要是修改以下這一步驟:
const output = isArray(input) ? [] : {}
主要利用 Object.create()
來創(chuàng)建 output
對象,改成這樣:
const initCloneObject = obj => {
// 處理基于 Object.create(null) 或 Object.create(Object.prototype.__proto__) 的實例對象
// 其中 Object.prototype.__proto__ 就是站在原型頂端的男人
// 但我留意到 Lodash 庫的 clone 方法對以上兩種情況是不處理的
if (obj.constructor === undefined) {
return Object.create(null)
}
// 處理自定義構(gòu)造函數(shù)的實例對象
if (typeof obj.constructor === 'function' && (obj !== obj.constructor || obj !== Object.prototype)) {
const proto = Object.getPrototypeOf(obj)
return Object.create(proto)
}
return {}
}
const output = isArray(input) ? [] : initCloneObject(input)
來看下打印結(jié)果署浩,可以看到 source
的原型對象已經(jīng)拷貝過來了:
再來看下 Object.create(null)
的情況揉燃,也是預(yù)期結(jié)果。
我們可以看到 Lodash 的 _.cloneDeep(Object.create(null))
深拷貝方法并沒有處理這種情況筋栋。當(dāng)然了炊汤,要拷貝這種數(shù)據(jù)結(jié)構(gòu)在實際應(yīng)用場景,真的少之又少...
關(guān)于 Lodash 拷貝方法為什么不實現(xiàn)這種情況弊攘,我找到了一個相關(guān)的 Issue #588:
A shallow clone won't do that as it's just
_.assign({}, object)
and a deep clone is loosely based on the structured cloning algorithm and doesn't attempt to clone inheritance or lack thereof.
四抢腐、優(yōu)化
綜上所述,完整但未優(yōu)化的深拷貝方法如下:
const deepCopy = source => {
// 創(chuàng)建一個 WeakMap 對象襟交,記錄已拷貝過的對象
const weakmap = new WeakMap()
// 獲取數(shù)據(jù)類型
const getClass = x => Object.prototype.toString.call(x)
// 判斷是否為數(shù)組
const isArray = arr => getClass(arr) === '[object Array]'
// 判斷是否為引用類型
const isObject = obj => obj !== null && (typeof obj === 'object' || typeof obj === 'function')
// 判斷是否為包裝對象
const isWrapperObject = obj => {
const theClass = getClass(obj)
const type = /^\[object (.*)\]$/.exec(theClass)[1]
return ['Boolean', 'Number', 'String', 'Symbol', 'BigInt', 'Date', 'Map', 'Set'].includes(type)
}
// 處理包裝對象
const handleWrapperObject = obj => {
const type = getClass(obj)
switch (type) {
case '[object Boolean]':
return Object(Boolean.prototype.valueOf.call(obj))
case '[object Number]':
return Object(Number.prototype.valueOf.call(obj))
case '[object String]':
return Object(String.prototype.valueOf.call(obj))
case '[object Symbol]':
return Object(Symbol.prototype.valueOf.call(obj))
case '[object BigInt]':
return Object(BigInt.prototype.valueOf.call(obj))
case '[object Date]':
return new Date(obj.valueOf()) // new Date(+obj)
case '[object Map]': {
const map = new Map()
obj.forEach((item, key) => {
map.set(key, copy(item))
})
return map
}
case '[object Set]': {
const set = new Set()
obj.forEach(item => {
set.add(copy(item))
})
return set
}
default:
return undefined
}
}
// 處理正則表達(dá)式
const handleRegExp = regex => {
const { source, flags, lastIndex } = regex
const re = new RegExp(source, flags)
re.lastIndex = lastIndex
return re
}
const initCloneObject = obj => {
if (obj.constructor === undefined) {
return Object.create(null)
}
if (typeof obj.constructor === 'function' && (obj !== obj.constructor || obj !== Object.prototype)) {
const proto = Object.getPrototypeOf(obj)
return Object.create(proto)
}
return {}
}
// 拷貝(遞歸思路)
const copy = input => {
if (typeof input === 'function' || !isObject(input)) return input
// 正則表達(dá)式
if (getClass(input) === '[object RegExp]') {
return handleRegExp(input)
}
// 針對已拷貝過的對象迈倍,直接返回(解決循環(huán)引用的問題)
if (weakmap.has(input)) {
return weakmap.get(input)
}
// 處理包裝對象
if (isWrapperObject(input)) {
return handleWrapperObject(input)
}
const output = isArray(input) ? [] : initCloneObject(input)
// 記錄每次拷貝的對象
weakmap.set(input, output)
for (let key in input) {
if (input.hasOwnProperty(key)) {
const value = input[key]
output[key] = copy(value)
}
}
// 處理以 Symbol 值作為屬性鍵的屬性
const symbolArr = Object.getOwnPropertySymbols(input)
if (symbolArr.length) {
for (let i = 0, len = symbolArr.length; i < len; i++) {
if (input.propertyIsEnumerable(symbolArr[i])) {
const value = input[symbolArr[i]]
output[symbolArr[i]] = copy(value)
}
}
}
return output
}
return copy(source)
}
接下來就是優(yōu)化工作了...
4.1 優(yōu)化一
我們上面使用到了 for...in
和 Object.getOwnPropertySymbols()
方法去遍歷對象的屬性(包括字符串屬性和 Symbol 屬性),還涉及了可枚舉屬性和不可枚舉屬性。
- for...in:遍歷自身和繼承過來的可枚舉屬性(不包括 Symbol 屬性)。
- Object.keys:返回一個數(shù)組座柱,包含對象自身所有可枚舉屬性(不包括不可枚舉屬性和 Symbol 屬性)
- Object.getOwnPropertyNames:返回一個數(shù)組,包含對象自身的屬性(包括不可枚舉屬性迹鹅,但不包括 Symbol 屬性)
- Object.getOwnPropertySymbols:返回一個數(shù)組,包含對象自身的所有 Symbol 屬性(包括可枚舉和不可枚舉屬性)
- Reflect.ownKeys:返回一個數(shù)組贞言,包含自身所有的屬性(包括 Symbol 屬性斜棚,不可枚舉屬性以及可枚舉屬性)
由于我們僅拷貝可枚舉的字符串屬性和可枚舉的 Symbol 屬性,因此我們將
Reflect.ownKeys()
和Object.prototype.propertyIsEnumerable()
結(jié)合使用即可该窗。
所以弟蚀,我們將以下這部分:
for (let key in input) {
if (input.hasOwnProperty(key)) {
const value = input[key]
output[key] = copy(value)
}
}
// 處理以 Symbol 值作為屬性鍵的屬性
const symbolArr = Object.getOwnPropertySymbols(input)
if (symbolArr.length) {
for (let i = 0, len = symbolArr.length; i < len; i++) {
if (input.propertyIsEnumerable(symbolArr[i])) {
const value = input[symbolArr[i]]
output[symbolArr[i]] = copy(value)
}
}
}
優(yōu)化成:
// 僅遍歷對象自身可枚舉的屬性(包括字符串屬性和 Symbol 屬性)
Reflect.ownKeys(input).forEach(key => {
if (input.propertyIsEnumerable(key)) {
output[key] = copy(input[key])
}
})
4.2 優(yōu)化二
優(yōu)化 getClass()
、isWrapperObject()
酗失、handleWrapperObject()
义钉、handleRegExp()
及其相關(guān)的類型判斷方法。
由于 handleWrapperObject()
原意是處理包裝對象级零,但是隨著后面要處理的特殊對象越來越多断医,為了減少文章篇幅,暫時都寫在里面了奏纪,稍微有點亂鉴嗤。
因此下面我們來整合一下,部分處理函數(shù)可能會修改函數(shù)名序调。
五醉锅、最終
其實,上面提到的一些邊界 Case发绢、或者其他一些特殊對象(如 ArrayBuffer
等)硬耍,這里并沒有處理,但我認(rèn)為該完結(jié)了边酒,因為這些在實際應(yīng)用場景真的太少了经柴。
代碼已丟到 GitHub ?? toFrankie/Some-JavaScript-File。
還是那句話:
如果生產(chǎn)環(huán)境使用
JSON.stringify()
無法解決你的需求墩朦,請使用 Lodash 庫的_.cloneDeep()
方法坯认,那個才叫面面俱到。千萬別用我這方法氓涣,切記牛哺!
這篇文章主要面向?qū)W習(xí)、面試(手動狗頭)劳吠,或許也可以幫助你熟悉一些對象的特性引润。如有不足,歡迎指出痒玩,萬分感謝 ?? ~
終于終于終于......要寫完了淳附,吐了三斤血...
最終版本如下:
const deepCopy = source => {
// 創(chuàng)建一個 WeakMap 對象,記錄已拷貝過的對象
const weakmap = new WeakMap()
// 獲取數(shù)據(jù)類型蠢古,返回值如:"Object"燃观、"Array"、"Symbol" 等
const getClass = x => {
const type = Object.prototype.toString.call(x)
return /^\[object (.*)\]$/.exec(type)[1]
}
// 判斷是否為數(shù)組
const isArray = arr => getClass(arr) === 'Array'
// 判斷是否為引用類型
const isObject = obj => obj !== null && (typeof obj === 'object' || typeof obj === 'function')
// 判斷是否為“特殊”對象(需要特殊處理)
const isSepcialObject = obj => {
const type = getClass(obj)
return ['Boolean', 'Number', 'String', 'Symbol', 'BigInt', 'Date', 'Map', 'Set', 'RegExp'].includes(type)
}
// 處理特殊對象
const handleSepcialObject = obj => {
const type = getClass(obj)
const Ctor = obj.constructor // 對象的構(gòu)造函數(shù)
const primitiveValue = obj.valueOf() // 獲取對象的原始值
switch (type) {
case 'Boolean':
case 'Number':
case 'String':
case 'Symbol':
case 'BigInt':
// 處理包裝對象 Wrapper Object
return Object(primitiveValue)
case 'Date':
return new Ctor(primitiveValue) // new Date(+obj)
case 'RegExp': {
const { source, flags, lastIndex } = obj
const re = new RegExp(source, flags)
re.lastIndex = lastIndex
return re
}
case 'Map': {
const map = new Ctor()
obj.forEach((item, key) => {
// 注意便瑟,即使 Map 對象的 key 為引用類型缆毁,這里也不能 copy(key),否則會失去引用到涂,導(dǎo)致該屬性無法訪問得到脊框。
map.set(key, copy(item))
})
return map
}
case 'Set': {
const set = new Ctor()
obj.forEach(item => {
set.add(copy(item))
})
return set
}
default:
return undefined
}
}
// 創(chuàng)建輸出對象(原型拷貝關(guān)鍵就在這一步)
const initCloneObject = obj => {
if (obj.constructor === undefined) {
return Object.create(null)
}
if (typeof obj.constructor === 'function' && (obj !== obj.constructor || obj !== Object.prototype)) {
const proto = Object.getPrototypeOf(obj)
return Object.create(proto)
}
return {}
}
// 拷貝方法(遞歸思路)
const copy = input => {
if (typeof input === 'function' || !isObject(input)) return input
// 針對已拷貝過的對象,直接返回(解決循環(huán)引用的問題)
if (weakmap.has(input)) {
return weakmap.get(input)
}
// 處理包裝對象
if (isSepcialObject(input)) {
return handleSepcialObject(input)
}
// 創(chuàng)建輸出對象
const output = isArray(input) ? [] : initCloneObject(input)
// 記錄每次拷貝的對象
weakmap.set(input, output)
// 僅遍歷對象自身可枚舉的屬性(包括字符串屬性和 Symbol 屬性)
Reflect.ownKeys(input).forEach(key => {
if (input.propertyIsEnumerable(key)) {
output[key] = copy(input[key])
}
})
return output
}
return copy(source)
}
六践啄、參考
The end.