問題
由于 js 的傳參方式有時會遇到這樣的場景:
function setTime(data) {
let result = {};
result.obj = data.obj || {};
result.obj.time = Date.now();
return result
}
let data = {
title:'loooook!',
obj: {
name: 'keo',
age: '12'
}
}
let res = setTime(data);
console.log('res',res);
//res { obj: { name: 'keo', age: '12', time: 1533625350183 } }
console.log('data',data);
//data { title: 'loooook!', obj: { name: 'keo', age: '12', time: 1533625350183 } }
我只是想繼承參數(shù)的部分?jǐn)?shù)據(jù),并在此基礎(chǔ)添加一些東西,但是參數(shù) data 的源數(shù)據(jù)也被我改動了,如果之后有其他人想要從data獲取數(shù)據(jù),他可能還需要注意是否有像 setTime 這樣的函數(shù)調(diào)用它。
一點(diǎn)修改
function setTime(data) {
let result = {};
result.obj = {};
Object.assign(result.obj,data.obj)
result.obj.time = Date.now();
return result
}
嗯,或者你也可以用 for...in,注意下二者的不同。
我們知道 Object.assign 只是淺拷貝,如果 data.obj 的屬性值仍然有引用類型的話,那么還是會遇見同樣的問題。
那要怎么辦?難道要遍歷data下每個屬性的值?一個個復(fù)制過來?我們看看 lodash 是怎么做的

你猜的沒錯,的確是要深度遍歷的。
在
baseClone方法內(nèi),拿到要拷貝的對象 value 后,先檢查其類型,然后由對應(yīng)的 handler 來處理,比如value是數(shù)組類型,則使 result 為同樣長度的數(shù)據(jù),然后對每一項(xiàng)都遞歸調(diào)用 baseClone,直到 value 是非引用類型,返回 value的值;如果是普通對象類型,則使 result 為空數(shù)組,然后拿取value的key,對每個key的賦值也是遞歸調(diào)用baseClone。
想要簡單點(diǎn)
難道我深拷貝一個變量還要引入 lodash 這么麻煩嗎 ?沒有簡單點(diǎn)的辦法嗎?
JSON.parse(JSON.stringify(param))
嗯,可能有點(diǎn)不是那么酷炫,但是他確實(shí)可以滿足要求,而且也無須引入其他的庫。但如果它真的這么完美,為什么 lodash 不這么寫呢?
的確,它的缺點(diǎn)還挺多的,這里取幾個我覺得比較重要的:
- Set 類型、Map 類型以及 Buffer 類型會被轉(zhuǎn)換成
{} - undefined、任意的函數(shù)以及 symbol 值,在序列化過程中會被忽略(出現(xiàn)在非數(shù)組對象的屬性值中時)或者被轉(zhuǎn)換成 null(出現(xiàn)在數(shù)組中時)
- 對包含循環(huán)引用的對象(對象之間相互引用,形成無限循環(huán))執(zhí)行此方法,會拋出錯誤
- 所有以 symbol 為屬性鍵的屬性都會被完全忽略掉,即便 replacer 參數(shù)中強(qiáng)制指定包含了它們
是啊,畢竟JSON的兩個方法本身就只是用來轉(zhuǎn)換 js 內(nèi)的對象為 JSON 格式的,上述幾點(diǎn)甚至都不是缺點(diǎn),是我們想借用其他方法做深拷貝時遇到的問題。
既然是問題那應(yīng)該可以解決吧,比如第一條和第二條,在 stringify 時判斷類型,轉(zhuǎn)化成 帶類型標(biāo)識符的對象字符串如:Set [1,2,3,4,5],然后在parse的時候?qū)ψ址M(jìn)行解析,特別的類型調(diào)用對應(yīng)的構(gòu)造函數(shù)... 聽起來變得更麻煩了,沒關(guān)系,忍忍把各個類型的處理都寫了;針對第三條,拋錯了?沒關(guān)系,我 try catch 包起來...,什么?循環(huán)引用?
循環(huán)引用?
function parse (param){
return JSON.parse(JSON.stringify(param))
}
var a = {}
var b = {}
a['b'] = b
b['a'] = a
console.log(parse(a))
//TypeError: Converting circular structure to JSON at JSON.stringify
如上代碼, 變量a 和 b 互相引用對方,此時如果借用 JSON 的方法來進(jìn)行深拷貝的話,會報循環(huán)結(jié)構(gòu)轉(zhuǎn)換轉(zhuǎn)換 JSON 錯誤。這個問題怎么解決呢?我們再翻出 lodash 的源碼看看...
// Check for circular references and return its corresponding clone.
stack || (stack = new Stack);
var stacked = stack.get(value);
if (stacked) {
return stacked;
}
stack.set(value, result);
這里的 value 和 result 分別是是一次遍歷中 要拷貝的值 和 拷貝的結(jié)果。stack 是一個用來儲存每次對應(yīng)的 value 和 result 的對象, stack下有一塊用于儲存的數(shù)組結(jié)構(gòu),該數(shù)組的每一項(xiàng)記錄了單次遍歷中的 value 和 result,后二者再次以數(shù)組的形式存儲,以 value 做為下標(biāo) 0 的項(xiàng),result 為下標(biāo) 1 的項(xiàng)(這里不用對象的 key-value 形式可能是因?yàn)檠h(huán)引用的變量無法使用 JSON.stringify 轉(zhuǎn)換成字符串,只能 toString 轉(zhuǎn)成 object Object);stack 是做為參數(shù)貫穿整個遍歷過程的,每次遍歷時都會以當(dāng)前的 value 值進(jìn)行查找(這里的查找直接是判斷內(nèi)存地址相等),如果能在 stack 中查到到對應(yīng)的結(jié)果,則直接返回記錄中的result,不再繼續(xù)遞歸。
好了,循環(huán)引用的問題我們解決了,鼓掌!但是我也放棄使用 JSON 方法了...還有沒有其他直接點(diǎn)的方法呢?
其他方法
結(jié)構(gòu)化克隆算法是由HTML5規(guī)范定義的用于復(fù)制復(fù)雜JavaScript對象的算法,它通過遞歸輸入對象來構(gòu)建克隆,同時保持先前訪問過的引用的映射,以避免無限遍歷循環(huán)。
怎么用?
emmm... 它還不能直接使用,你得依靠一些其他的 API ,間接的使用它。
postMessage()
function StructuredClone(param) {
return new Promise(function (res, rej) {
const {port1, port2} = new MessageChannel();
port2.onmessage = ev => res(ev.data);
port1.postMessage(param);
})
}
StructuredClone(objects).then(result => console.log(result))
什么??還是異步的... 不,我希望能使用同步的方法使用它。
history()
function structuralClone(obj) {
const oldState = history.state;
history.replaceState(obj, document.title);
const copy = history.state;
history.replaceState(oldState, document.title);
return copy;
}
const clone = structuralClone(objects);
如你所見,我們要借用一下 history.replaceState 這個方法,但是我們不能改變 history 原有的狀態(tài),所以用完就要恢復(fù)原狀,當(dāng)無事發(fā)生過。
至少,這是個同步的方法...,如果是同步的場景可以考慮一下...
性能展示
這里的測試代碼是使用的 [Deep-copying in JavaScript] (https://dassur.ma/things/deep-copy/) 一文中的,并再次基礎(chǔ)做了一些修改。
結(jié)果! (很懶就不畫圖表了)
單位 μs (繆斯),計算時間的用的接口是 performance.now()結(jié)果精確到5微秒。
-
chrome
chrome -
safari
...em...Safari瀏覽器在調(diào)用完 postMessage 方法后就...沒有然后了...表格都沒刷出來...等了 40 s 終于刷出第一欄...
注釋完postMessage又發(fā)現(xiàn)不能頻繁的調(diào)用 history 。
調(diào)用 history 的 api 拋異常

- firefox
...em.. 調(diào)用 history 相關(guān) api 對 firefox 好像壓力很大,以至于循環(huán)都有些錯亂...于是注釋了相關(guān)代碼

就結(jié)果而言好像看不出什么區(qū)別,可能是我的數(shù)據(jù)不好,大家可以去看看原文,有展示閱讀性更好的圖表,盡管沒有 lodash 就是了。
結(jié)果
回到我們最初的問題,我們只是想深拷貝一個 js 對象,如果只是一個比較"普通"的對象,用JSON的方法簡單又快捷,但是如果這個對象有些“復(fù)雜”,似乎使用 lodash 的方法是比較好的選擇,而且 lodash 連 Structured Clone 算法忽視的 symbol 類型 和 Function 也考慮其中,兼容性也沒問題,也不會在不同的瀏覽器發(fā)生意外的狀況...
lodash 萬歲!lol!!

