由簡入繁
今天花點時間,來整理一波深複製的skr操作。由簡入繁 實現一個不會循環引用的deepClone方法
深複製&&潛複製
文章開頭,先掃個盲,仔細看下面這個例子
var obj = {a: 1, b: 2, c: { a: 3 },d: [4, 5]}
var obj1 = obj
var obj2 = JSON.parse(JSON.stringify(obj))//深拷貝常用方法
var obj3 = {...obj}
var obj4 = Object.assign({},obj)
obj.a = 999
obj.c.a = -999
obj.d[0] = 123
console.log(obj1) //{a: 999, b: 2, c: { a: -999 },d: [123, 5]}
console.log(obj2) //{a: 1, b: 2, c: { a: 3 },d: [4, 5]}
console.log(obj3) //{a: 1, b: 2, c: { a: -999 },d: [123, 5]}
console.log(obj4) //{a: 1, b: 2, c: { a: -999 },d: [123, 5]}
發現什麼問題了嗎?這些操作好像只是遍歷了樹的第一層,然後將指針指向了新的對象
- 這裏有個知識點:有人可能會覺得基本類型 是在棧中開闢一個新空間存放,其實obj.a 和obj1.a 開始用的是同一個‘1’,後面進行obj.a=111操作的時候 開闢一個新的空間存放999,obj1.a還是存放‘1’
單純可愛001
以下代碼可能會忽略類型校驗、各種兼容,只要看思路就好0.0
// 處理對象 var obj = { a: {b: {c: 123}} }
function clone1 (obj) {
var result = {}
for(var i in obj) {
if (obj.hasOwnProperty(i)) {
if (typeof obj[i] === 'object') {
result[i] = clone(obj[i])
} else {
result[i] = obj[i]
}
}
}
return result
}
- 這裏的類型判斷有點問題,完整版本看這裏
- 想知道爲什麼用hasOwnProperty,請戳這裏
- 其實還可以思考下,如果兼容數組 要怎麼做,如果是es6的 還會有set、map、weakset、weakmap
上面這這種方法 長相醜陋,而且在深度優先或者循環引用的時候 容易棧溢出(貌似廣度優先不會),所以我們要想個辦法
暫時思路是:破壞引用或者做循環引用檢查,我們接下來會重點介紹前者。
簡單粗暴002
項目中,很多人會通過JSON提供的兩個方法來實現深複製
JSON.parse(JSON.stringify(obj))
var a = {}
a.a = a
JSON.parse(JSON.stringify(a)) // Uncaught TypeError: Converting circular structure to JSON
但是通過測試發現,複製深度是1000000的對象,直接棧溢出了,說明底層也是使用的遞歸的方式。而且還一個有趣的現象,JSON內部做了循環引用檢查
- JSON不可以複製對象的方法,莫慌 猛戳這裏找黑科技
高冷帥氣003
想要破解遞歸爆棧,方法只有一個:不用遞歸。我們可以嘗試下上學時候老師教的鏈表還有堆棧
我們把一個對象旋轉90°,像極了一棵樹,那麼我們面對的問題就成了 複製一個樹
鏈表太複雜了,還有記錄指針,我們可以通過棧+js對象這個技巧,規避掉還原樹的時候 找不到節點順序關係的難題,假裝看不到註釋的代碼
function deepClone (obj) {
// const store = []
let root = Array.isArray(obj) ? [] : {}
const loopList = [{parent: root, key: undefined, data: obj}]
while (loopList.length) {
// 深度優先 廣度優先沒影響吧
const node = loopList.pop()
const parent = node.parent
const key = node.key
const data = node.data
let resource = parent
if (typeof key !== 'undefined') {
// 如果只是對象 這裏可以使用連續賦值
resource = parent[key] = Array.isArray(data) ? [] : {}
}
// let exitedData = find(store, data)
// if (exitedData) {
// parent[key] = exitedData.target
// continue
// }
// store.push({ data, target: resource })
for (let k in data) {
if (data.hasOwnProperty(k)) {
if (typeof data[k] === 'object') {
loopList.push({parent: resource, key: k, data: data[k]})
} else {
resource[k] = data[k]
}
}
}
}
return root
}
var O = {
aaa: {bbb: {ccc: 456},},
ddd: [123, 456, 789]
}
var A = [
{aaa: {bbb: {ccc: 456},}},
132
]
var ooo = deepClone(O)
var aaa = deepClone(A)
目前情況是:遞歸沒有了,但還是沒有破除循環引用。最蛋疼的是還出現了一個新的問題,關係丟失,具體如下
var a = {}
var b = { c: a, d: a}
var e = clone(b)
e.c === e.d // false
然後我們想到,可不可以找一個store做緩存,一來可以打破循環引用,二來 還可以空間換時間 騷秀一波
打開註釋,見證奇蹟的時刻到了 skr skr skr
騷完之後,我們迴歸現實 看看這個方法的缺點:
- 如果你不想保持對象的關聯關係,不要用此方法
- 如果對象數量很多,不要用此方法
附錄
Types方法
var type = (param) => {
var checker = Object.prototype.toString
var getName = () => {
return checker.call(param).slice(8).slice(0, -1).toLowerCase()
}
var is = (typeName) => {
return checker.call(param) === `[object ${typeName}]`
}
var isOneOf = (names = []) => {
return names.some(name => checker.call(param) === `[object ${name}]`)
}
var isEvery = (names = []) => {
return names.every(name => checker.call(param) === `[object ${name}]`)
}
var isNot = (typeNames = []) => {
var checkers = {
string: () => !is(typeNames),
array: () => typeNames.every(name => !is(name))
}
return checkers[getName(typeNames)]()
}
// 暫時閉包夠用了,後面用涉及到this了 記得bind
return { is, isOneOf, isEvery, isNot, name: getName() }
}
for-in、Object.keys、Object.getOwnPropertyNames的用法
- for-in:輸出原型鏈+自身所有可枚舉的屬性
- keys:輸出自身可枚舉屬性,就是for-in + hasOwnProperty
- getOwnPropertyNames:輸出自身全部屬性
測試深度優先+廣度優先
function dataAction(deep, breadth) {
var data = {}, pointer = data
for (var i = 0; i < deep; i++) {
pointer = pointer['data'] = {}
for (var j = 0; j < breadth; j++) {
pointer[j] = j
}
}
return data
}
// test
clone1(dataAction(n,m))
JSON黑科技
// 這個 parse 比較牛逼 可以不丟失function
// 注意:不要搞太大的對象 storage最大5M
const JSONBooster = {
stringify: (toJson) => {
return JSON.stringify(toJson, function (key, val) {
if (typeof val === 'function') { return `0?0:${val}` }
return val
})
},
parse: (str) => {
return JSON.parse(str, function (k, v) {
if (v.indexOf && v.indexOf('function') > -1) {
return evil(`(function(){return ${v} })()`)
}
return v
})
}
}
// eval的代替方案 貌似可行
const evil = (fn) => {
var Fn = Function
return new Fn('return ' + fn)()
}