从一道春招笔试题提及 [上]

先来看这样一道题: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 (也就是 nullundefined) 赋值,因此这里用三目运算符进行了包裹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 } } }
复制代码

运行代码,问题依然存在。设计

查阅 MDNObject.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 操做也只进行了一层遍历,并无递归的处理类型为 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 实现的相比显然是不完善的,但根据题设,这里只存在对象和基本类型,因此这种简易实现应该也够用了。以上即是我对这道题的完整解题思路,若有任何问题或者好的建议,还请你们不吝指正。

参考连接

相关文章
相关标签/搜索