從一道春招筆試題說起 [上]

Mitscherlich發表於2019-04-01

先來看這樣一道題:

給定一個字典(物件),假設其中部分鍵值是有 '.' 號的字串,試設計一個 nested 函式,使得其變成一個巢狀物件 (假設不存在重複鍵值)。

示例:

給定物件:

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 替代了原來賦值操作:

// 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 (也就是 nullundefined) 賦值,所以這裡用三目運算子進行了包裹

執行結果:
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 } } }
複製程式碼

執行程式碼,問題依然存在。

查閱 MDNObject.assign 確實可以用於合併物件,而且淺層物件(≤2)的合併也都沒有問題。

這使我想到了 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 操作也只進行了一層遍歷,並沒有遞迴的處理型別為 objectvalue 值,使得深度 ≥ 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 的原始碼,lodashmerge 操作是通過不斷遞迴深拷貝來實現物件合併的,這樣就不存在覆蓋問題,例如:

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 實現的相比顯然是不完善的,但根據題設,這裡只存在物件和基本型別,所以這種簡易實現應該也夠用了。以上便是我對這道題的完整解題思路,如有任何問題或者好的建議,還請大家不吝指正。

參考連結

相關文章