先來看這樣一道題:git
給定一個字典(對象),假設其中部分鍵值是有 '.' 號的字符串,試設計一個
nested
函數,使得其變成一個嵌套對象 (假設不存在重複鍵值)。github
給定對象:數組
const obj = {
'A': 1,
'B.A': 2,
'B.B': 3,
'CC.D.E': 4,
'CC.D.F': 5
}
複製代碼
應獲得嵌套對象:瀏覽器
const nestedObj = {
'A': 1,
'B': {
'A': 2,
'B': 3
},
'CC': {
'D': {
'E': 4,
'F': 5
}
}
}
複製代碼
題目其實很簡單,考察的也就是基本的 JS 字符串和數組操做,我很快實現了這樣一種代碼:函數
// version 1.0
const nested = obj => {
const res = {}
for (const key of Object.keys(obj)) {
if (key.indexOf('.') > -1) {
const [target, ...newKey] = key.split('.')
res[target] = nested({ [newKey.join('.')]: obj[key] }) // 遞歸處理剩餘部分
} else res[key] = obj[key]
}
return res
}
複製代碼
const obj = { 'A': 1, 'B.A': 2, 'B.B': 3, 'CC.D.E': 4, 'CC.D.F': 5 }
console.log(nested(obj))
// { A: 1, B: { B: 3 }, CC: { D: { F: 5 } } }
複製代碼
運行代碼,很快發現了問題:這樣直接賦值顯然會覆蓋掉已將存在的深層對象。這顯然是不合理的,因而我使用 Object.assign
替代了原來賦值操做:ui
// version 1.1
const nested = obj => {
const res = {}
for (const key of Object.keys(obj)) {
if (key.indexOf('.') > -1) {
const [target, ...newKey] = key.split('.')
- res[target] = nested({ [newKey.join('.')]: obj[key] }) // 遞歸處理剩餘部分
+ res[target] = Object.assign(
+ res[target] ? res[target] : {},
+ nested({ [newKey.join('.')]: obj[key] }) // 遞歸處理剩餘部分
+ )
} else res[key] = obj[key]
}
return res
}
複製代碼
注意
Object.assign
不能向Nil
(也就是null
和undefined
) 賦值,因此這裏用三目運算符進行了包裹spa
const obj = { 'A': 1, 'B.A': 2, 'B.B': 3, 'CC.D.E': 4, 'CC.D.F': 5 }
console.log(nested(obj))
// { A: 1, B: { B: 3 }, CC: { D: { F: 5 } } }
複製代碼
運行代碼,問題依然存在。設計
查閱 MDN,Object.assign
確實能夠用於合併對象,並且淺層對象(≤2)的合併也都沒有問題。code
這使我想到了 lodash
提供的 merge
方法,先拿來主義一下:對象
// version 1.2
+ const { merge } = require('lodash')
const nested = obj => {
const res = {}
for (const key of Object.keys(obj)) {
if (key.indexOf('.') > -1) {
const [target, ...newKey] = key.split('.')
- res[target] = Object.assign(
+ res[target] = merge(
+ res[target] ? res[target] : {},
+ nested({ [newKey.join('.')]: obj[key] }) // 遞歸處理剩餘部分
+ )
} else res[key] = obj[key]
}
return res
}
複製代碼
const obj = { 'A': 1, 'B.A': 2, 'B.B': 3, 'CC.D.E': 4, 'CC.D.F': 5 }
console.log(nested(obj))
// { A: 1, B: { A: 2, B: 3 }, CC: { D: { E: 4, F: 5 } } }
複製代碼
運行代碼,成功!正確返回了預期的結果,可這是爲何呢?
繼續查閱 MDN,瀏覽到 Polyfill 一節,這裏是爲了使不能原生支持 assign
的瀏覽器用上這個函數,大致上能夠看做 assign
的源代碼。能夠看到,其實 assign
操做也只進行了一層遍歷,並無遞歸的處理類型爲 object
的 value
值,使得深度 ≥ 1 的對象依然被覆蓋;從新瀏覽文檔,在深拷貝問題一節也確實提到了對象的覆蓋問題,例如:
const obj1 = { A: 1, B: { C: 2 } }
const obj1 = { A: 2, B: { D: 3 } }
console.log(Object.assign({}, obj1, obj2)) // ==> { A: 2, B: { D: 3 } }
// B.C 丟失了,由於前一個對象中的 { B: [Object] }
// 被後續的對象中的 { B: [Object] } 覆蓋了
複製代碼
到 Github 翻閱 lodash.merge
的源碼,lodash
的 merge
操做是經過不斷遞歸深拷貝來實現對象合併的,這樣就不存在覆蓋問題,例如:
const { merge } = require('lodash')
const obj1 = { A: 1, B: { C: 2 } }
const obj1 = { A: 2, B: { D: 3 } }
console.log(merge({}, obj1, obj2)) // ==> { A: 2, B: { C: 2, D: 3 } }
// B.C 被正確合併了
複製代碼
基於這個思路,我簡單實現了一個 merge
函數,修改原代碼以下:
// version 2.0
const baseMerge = (target, from) => {
const [newTarget, ...newFrom] = from
if (newFrom.length > 1) {
return baseMerge(target, [baseMerge(newTarget, newFrom)])
} else {
const keys = Object.keys(newTarget)
for (const key of keys) {
if (target.hasOwnProperty(key)) {
if (typeof target[key] === 'object') {
baseMerge(target[key], [newTarget[key]])
} else {
target[key] = newTarget[key]
}
} else target[key] = newTarget[key]
}
return target
}
}
const merge = (target, ...from) => baseMerge(target, Array.from(from))
const nested = obj => {
const res = {}
for (const key of Object.keys(obj)) {
if (key.indexOf('.') > -1) {
const [target, ...newKey] = key.split('.')
res[target] = merge(
res[target] ? res[target] : {},
nested({ [newKey.join('.')]: obj[key] }) // 遞歸處理剩餘部分
)
} else res[key] = obj[key]
}
return res
}
複製代碼
const obj = { 'A': 1, 'B.A': 2, 'B.B': 3, 'CC.D.E': 4, 'CC.D.F': 5 }
console.log(nested(obj))
// { A: 1, B: { A: 2, B: 3 }, CC: { D: { E: 4, F: 5 } } } // 成功!
複製代碼
固然,這個 merge
函數與 lodash
實現的相比顯然是不完善的,但根據題設,這裏只存在對象和基本類型,因此這種簡易實現應該也夠用了。以上即是我對這道題的完整解題思路,若有任何問題或者好的建議,還請你們不吝指正。