如何将这个嵌套对象转换为扁平化对象?

31

抱歉,我不知道如何表达问题标题。请尽可能帮忙编辑。

我有一个像这样的对象:

{
    a: 'jack',
    b: {
        c: 'sparrow',
        d: {
           e: 'hahaha'
        }
    }
}
我想让它看起来像:
{
    'a': 'jack',
    'b.c': 'sparrow',
    'b.d.e': 'hahaha'
}

// so that I can use it this way:
a['b.d.e']

jQuery也可以。我知道对于嵌套对象,我可以使用a.b.d.e来获取hahaha,但今天我必须像这样使用它a['b.d.e'] -_-!!!如何实现?提前致谢 :)

16个回答

41

您可以使用递归函数来遍历对象并将其扁平化。

var test = {
    a: 'jack',
    b: {
        c: 'sparrow',
        d: {
            e: 'hahaha'
        }
    }
};

function traverseAndFlatten(currentNode, target, flattenedKey) {
    for (var key in currentNode) {
        if (currentNode.hasOwnProperty(key)) {
            var newKey;
            if (flattenedKey === undefined) {
                newKey = key;
            } else {
                newKey = flattenedKey + '.' + key;
            }

            var value = currentNode[key];
            if (typeof value === "object") {
                traverseAndFlatten(value, target, newKey);
            } else {
                target[newKey] = value;
            }
        }
    }
}

function flatten(obj) {
    var flattenedObject = {};
    traverseAndFlatten(obj, flattenedObject);
    return flattenedObject;
}

var flattened = JSON.stringify(flatten(test));
console.log(flattened);

如果需要的话,一个嵌套的循环结构可以用来反转这个过程。不过也许有更简洁的方法:

var test = {'a':'jack','b.c':'sparrow','b.d.e':'hahaha'};

function expand(target, keySeparator) {
    var result = {};
    for (var key in target) {
        if (target.hasOwnProperty(key)) {
          var nestedKeys = key.split(keySeparator);
          // Get the last subKey
          var leaf = nestedKeys[nestedKeys.length - 1];
          // Get all subKeys except for the last
          var branch = nestedKeys.slice(0, nestedKeys.length - 1);
          
          var currentTarget = result;
          for (var i = 0; i < branch.length; i += 1) {
            var subKey = nestedKeys[i];
            // If this is the first time visiting this branch, we need to instantiate it
            if (currentTarget[subKey] === undefined) {
              currentTarget[subKey] = {};
            }
            // Visit the branch
            currentTarget = currentTarget[subKey];
          }
          currentTarget[leaf] = target[key];
        }
    }
    return result;
}

var expanded = JSON.stringify(expand(test, "."));
console.log(expanded);


有人有它的反向吗? - Hemant Rajpoot
@HemantRajpoot 我编辑了这个答案,并加入了一个简单的反转。尽管可能有更简洁的方法来完成这个任务。 - Marie

17

一种替代的递归实现。尽管现有的实现已经非常好了,但我仍然想写一个自己的实现。

递归函数检查键是否为'object'类型。

  • 如果是对象,我们对每个对象的键进行迭代。
  • 否则,将其添加到我们的结果对象中。
function flat(res, key, val, pre = '') {
  const prefix = [pre, key].filter(v => v).join('.');
  return typeof val === 'object'
    ? Object.keys(val).reduce((prev, curr) => flat(prev, curr, val[curr], prefix), res)
    : Object.assign(res, { [prefix]: val});
}
return Object.keys(input).reduce((prev, curr) => flat(prev, curr, input[curr]), {});

扁平化 NPM 包

你也可以简单地使用扁平化 NPM 包,这是一个广为人知、经过测试的库。

var flatten = require('flat')
flatten(obj);

⬑ 我会在重要的代码中使用这个。

[额外] 更简洁的调用上面的函数

function flatObject(input) {
  function flat(res, key, val, pre = '') {
    const prefix = [pre, key].filter(v => v).join('.');
    return typeof val === 'object'
      ? Object.keys(val).reduce((prev, curr) => flat(prev, curr, val[curr], prefix), res)
      : Object.assign(res, { [prefix]: val});
  }

  return Object.keys(input).reduce((prev, curr) => flat(prev, curr, input[curr]), {});
}

const result = flatObject(input);

[额外] 演示

http://codepen.io/zurfyx/pen/VpErja?editors=1010

function flatObject(input) {
  function flat(res, key, val, pre = '') {
    const prefix = [pre, key].filter(v => v).join('.');
    return typeof val === 'object'
      ? Object.keys(val).reduce((prev, curr) => flat(prev, curr, val[curr], prefix), res)
      : Object.assign(res, { [prefix]: val});
  }

  return Object.keys(input).reduce((prev, curr) => flat(prev, curr, input[curr]), {});
}

const result = flatObject({
    a: 'jack',
    b: {
        c: 'sparrow',
        d: {
           e: 'hahaha'
        }
    }
});

document.getElementById('code').innerHTML = JSON.stringify(result, null, 2);
<pre><code id="code"></code></pre>


稍微调整一下,使用函数符号来定义,并添加检查输入是否为 JSON 字符串而不是对象。 function flat(res, key, val, pre = '') { const prefix = [pre, key].filter(v => v).join('_'); return typeof val === 'object' ? Object.keys(val).reduce((prev, curr) => flat(prev, curr, val[curr], prefix), res) : Object.assign(res, { [prefix]: val}); } if (typeof input === 'string') { input = JSON.parse(input); } return Object.keys(input).reduce((prev, curr) => flat(prev, curr, input[curr]), {}); }``` - Shanerk
对于未来的读者,我不建议使用此函数。它无法处理以下输入:字符串、数字、布尔值、未定义、null、日期;它也无法处理叶子节点:日期、其他类对象。 - Patrick Michaelsen
这个处理程序包括测试,并处理这些情况:https://github.com/prmichaelsen/parm/blob/e4b5eaadc03869d9e73a51759f459a8730f17f7e/libs/util/src/lib/flatten.ts 和 https://github.com/prmichaelsen/parm/blob/e4b5eaadc03869d9e73a51759f459a8730f17f7e/libs/util/src/lib/flatten.spec.ts - Patrick Michaelsen

15
你可以循环遍历对象的entries。如果value是一个对象,就递归调用该函数。使用flatMap获取一个扁平化的条目数组。
然后使用Object.fromEntries()从扁平化的条目数组中获取一个对象。

const input = {
  a: 'jack',
  b: {
    c: 'sparrow',
    d: {
      e: 'hahaha'
    }
  }
}

const getEntries = (o, prefix = '') => 
  Object.entries(o).flatMap(([k, v]) => 
    Object(v) === v  ? getEntries(v, `${prefix}${k}.`) : [ [`${prefix}${k}`, v] ]
  )

console.log(
  Object.fromEntries(getEntries(input))
)

注意: Object(v) === v 只对对象返回 true。对于 v = nulltypeof v === 'object' 也是 true。


请注意,这个非常好的答案认为日期是对象,而在这种情况下,日期应该被视为值。 - Kuba Wyrostek

6
递归是这种情况的最佳解决方案。

function flatten(input, reference, output) {
  output = output || {};
  for (var key in input) {
    var value = input[key];
    key = reference ? reference + '.' + key : key;
    if (typeof value === 'object' && value !== null) {
      flatten(value, key, output);
    } else {
      output[key] = value;
    }
  }
  return output;
}
var result = flatten({
  a: 'jack',
  b: {
    c: 'sparrow',
    d: {
      e: 'hahaha'
    }
  }
});
document.body.textContent = JSON.stringify(result);


4

选项1:导出仅包含叶子节点的扁平对象,即导出的对象仅包含以原始值结尾的路径(见示例)。

//recursion: walk on each route until the primitive value.
//Did we found a primitive?
//Good, then join all keys in the current path and save it on the export object.
export function flatObject(obj) {
    const flatObject = {};
    const path = []; // current path

    function dig(obj) {
        if (obj !== Object(obj))
            /*is primitive, end of path*/
            return flatObject[path.join('.')] = obj; /*<- value*/ 
    
        //no? so this is an object with keys. go deeper on each key down
        for (let key in obj) {
            path.push(key);
            dig(obj[key]);
            path.pop();
        }
    }

    dig(obj);
    return flatObject;
}

例子

let  obj = {aaa:{bbb:{c:1,d:7}}, bb:{vv:2}}
console.log(flatObject(obj))
/*
{
  "aaa.bbb.c": 1,
  "aaa.bbb.d": 7,
  "bb.vv": 2
}
*/

选项2:导出一个带有所有中间路径的扁平对象(见示例),更加简短明了。
export function flatObject(obj) {
    const flatObject = {};
    const path = []; // current path

    function dig(obj) {
        for (let key in obj) {
            path.push(key);
            flatObject[path.join('.')] = obj[key];
            dig(obj[key])
            path.pop();
        }
    }

    dig(obj);
    return flatObject;
}

示例:

let  obj = {aaa:{bbb:{c:1,d:7}}, bb:{vv:2}}
console.log(flatObject(obj))
/*{
  "aaa": {
    "bbb": {
      "c": 1,
      "d": 7
    }
  },
  "aaa.bbb": {
    "c": 1,
    "d": 7
  },
  "aaa.bbb.c": 1,
  "aaa.bbb.d": 7,
  "bb": {
    "vv": 2
  },
  "bb.vv": 2
}
*/

3
使用一个参数来表示父键的递归方法。

const
    getValues = (object, parents = []) => Object.assign({}, ...Object
        .entries(object)
        .map(([k, v]) => v && typeof v === 'object'
            ? getValues(v, [...parents, k])
            : { [[...parents, k].join('.')]: v }
        )
    ),
    object = { a: 'jack', b: { c: 'sparrow', d: { e: 'hahaha' } } };

console.log(getValues(object));


2
另一种使用ES6的方法。
const obj = {
  a: "jack",
  b: {
    c: "sparrow",
    d: {
      e: "hahaha"
    }
  }
};

function flattenObj(value, currentKey) {
  let result = {};

  Object.keys(value).forEach(key => {

    const tempKey = currentKey ? `${currentKey}.${key}` : key;

    if (typeof value[key] !== "object") {
      result[tempKey] = value[key];
    } else {
      result = { ...result, ...flattenObj(value[key], tempKey) };
    }
  });

  return result;
}

console.log(flattenObj(obj));

1

以下是如何使用ES6特性实现的示例。

const flatObject = obj => {
    const keys = Object.keys(obj)

    return keys.reduce((acc, k) => {
        const value = obj[k]

        return typeof value === 'object' ?
             {...acc, ...ObjectUtils.flatObject(value)} :
             {...acc, [k]: value}
    } , {})
}

2
ObjectUtils来自哪里? - Rafael Rozon
2
我猜这一行应该是 {...acc, ...flatObject(value)} : - 作为一个递归调用。 - muescha

1
这里有一个简单的解决方案 -
function flatObj(obj, newObj, parentKey) {
    for(let key in obj) {

        const currKey = parentKey.length > 0 ? `${parentKey}.${key}` : key

        if (typeof obj[key] === "object") {
            flatObj(obj[key], newObj, currKey);
        } else {
            newObj[currKey] = obj[key];
        }
    }

    return newObj;
};

    let obj = {
       a: 'jack',
       b: {
          c: 'sparrow',
          d: {
             e: 'hahaha'
          }
       }
    };

console.log(flatObj(obj, {}, ""));

1

排除数组和日期的解决方案

所有的解决方案都缺乏提供不扁平化数组的方法。 由于在JavaScript中,数组被视为对象,Object(Array) === Array会返回true。这会引发问题。

例如:

function traverseAndFlatten(currentNode, target, flattenedKey) {
  for (var key in currentNode) {
    if (currentNode.hasOwnProperty(key)) {
      var newKey;
      if (flattenedKey === undefined) {
        newKey = key;
      } else {
        newKey = flattenedKey + '.' + key;
      }

      var value = currentNode[key];
      if (Object(value) === value) {
        if (Object.keys(value).length === 0) {
          target[newKey] = {};
        }
        traverseAndFlatten(value, target, newKey);
      } else {
        target[newKey] = value;
      }
    }
  }

}

function flatten(obj) {
  var flattenedObject = {};
  traverseAndFlatten(obj, flattenedObject);
  return flattenedObject;
}

console.log('flattened object :', flatten({
  a: 'aaa',
  b: {
    cc: ['11', '22', '33'],
    d: {
      e: null,
      f: new Date()
    }
  }
}))

//output flattened object : {
//  "a": "aaa",
//  "b.cc.0": "11",
//  "b.cc.1": "22",
//  "b.cc.2": "33",
//  "b.d.e": null,
//  "b.d.f": {}
//}

我们可以看到,Array和Date被搞乱了。
为了避免这种情况,不要仅仅使用Object(Array) === Array作为条件,而是使用Object(value) === value && !Array.isArray(value) && !(value instanceof Date)
此外,可以使用typeof value === 'object' && value !== null && !Array.isArray(value) && !(value instanceof Date),它与之前的条件具有相同的作用。 !(value instanceof Date)用于排除Date,因为JavaScript将Date视为对象。
展开和扁平化的示例代码解决方案:

function traverseAndFlatten(currentNode, target, flattenedKey) {
  for (var key in currentNode) {
    if (currentNode.hasOwnProperty(key)) {
      var newKey;
      if (flattenedKey === undefined) {
        newKey = key;
      } else {
        newKey = flattenedKey + '.' + key;
      }

      var value = currentNode[key];
      if (Object(value) === value && !Array.isArray(value) && !(value instanceof Date)) {
        if (Object.keys(value).length === 0) {
          target[newKey] = {};
        }
        traverseAndFlatten(value, target, newKey);
      } else {
        target[newKey] = value;
      }
    }
  }

}

function flatten(obj) {
  var flattenedObject = {};
  traverseAndFlatten(obj, flattenedObject);
  return flattenedObject;
}

console.log('flattened object :', flatten({
  a: 'aaa',
  b: {
    cc: ['11', '22', '33'],
    d: {
      e: null,
      f: new Date()
    }
  }
}))

//Output
//flattened object: {
//  "a": "aaa",
//  "b.cc": [
//    "11",
//    "22",
//    "33"
//  ],
//  "b.d.e": null,
//  "b.d.f": "2023-09-27T03:38:02.963Z"
//}

//To expand the flattened object

function expand(target, keySeparator) {
  var result = {};
  for (var key in target) {
    if (target.hasOwnProperty(key)) {
      var nestedKeys = key.split(keySeparator);
      var leaf = nestedKeys[nestedKeys.length - 1];
      var branch = nestedKeys.slice(0, nestedKeys.length - 1);
      var currentTarget = result;
      for (var i = 0; i < branch.length; i += 1) {
        var subKey = nestedKeys[i];
        if (currentTarget[subKey] === undefined) {
          currentTarget[subKey] = {};
        }
        currentTarget = currentTarget[subKey];
      }
      currentTarget[leaf] = target[key];
    }
  }
  return result;
}

console.log('expanded object: ', expand({
  "a": "aaa",
  "b.cc": [
    "11",
    "22",
    "33"
  ],
  "b.d.e": null,
  "b.d.f": "2023-09-27T03:20:06.511Z"
}, '.'))

//Output
//expanded object:  {
//  "a": "aaa",
//  "b": {
//    "cc": [
//      "11",
//      "22",
//      "33"
//    ],
//    "d": {
//      "e": null,
//      "f": "2023-09-27T03:20:06.511Z"
//    }
//  }
//}


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接