如何将菊花链/点符号中的 JavaScript 对象展开为具有嵌套对象和数组的对象?

Posted

技术标签:

【中文标题】如何将菊花链/点符号中的 JavaScript 对象展开为具有嵌套对象和数组的对象?【英文标题】:How to unflatten a JavaScript object in a daisy-chain/dot notation into an object with nested objects and arrays? 【发布时间】:2017-07-30 10:00:06 【问题描述】:

我想像这样展开一个对象...

var obj2 = 
    "firstName": "John",
    "lastName": "Green",
    "car.make": "Honda",
    "car.model": "Civic",
    "car.revisions.0.miles": 10150,
    "car.revisions.0.code": "REV01",
    "car.revisions.0.changes": "",
    "car.revisions.1.miles": 20021,
    "car.revisions.1.code": "REV02",
    "car.revisions.1.changes.0.type": "asthetic",
    "car.revisions.1.changes.0.desc": "Left tire cap",
    "car.revisions.1.changes.1.type": "mechanic",
    "car.revisions.1.changes.1.desc": "Engine pressure regulator",
    "visits.0.date": "2015-01-01",
    "visits.0.dealer": "DEAL-001",
    "visits.1.date": "2015-03-01",
    "visits.1.dealer": "DEAL-002"
;

... 到具有嵌套对象和数组的对象中,如下所示:


  firstName: 'John',
  lastName: 'Green',
  car: 
    make: 'Honda',
    model: 'Civic',
    revisions: [
       miles: 10150, code: 'REV01', changes: '',
       miles: 20021, code: 'REV02', changes: [
         type: 'asthetic', desc: 'Left tire cap' ,
         type: 'mechanic', desc: 'Engine pressure regulator' 
      ] 
    ]
  ,
  visits: [
     date: '2015-01-01', dealer: 'DEAL-001' ,
     date: '2015-03-01', dealer: 'DEAL-002' 
  ]

这是我的(失败的)尝试:

function unflatten(obj) 
    var result = ;

    for (var property in obj) 
        if (property.indexOf('.') > -1) 
            var substrings = property.split('.');

            console.log(substrings[0], substrings[1]);


         else 
            result[property] = obj[property];
        
    

    return result;
;

我很快就开始不必要地重复代码,以便进行对象和数组的嵌套。这绝对是需要递归的东西。有什么想法吗?

编辑:我还在another question 中提出了相反的问题,即扁平化。

【问题讨论】:

我在这里有一种强烈的似曾相识。我可以发誓我昨天读过这个问题。 -- 编辑:好吧,这与yesterday's question相反。 上一个问题是关于flatten,这一个是关于unflatten。感觉像是一些家庭任务 【参考方案1】:

您可以先使用for...in 循环来循环对象属性,然后在. 处拆分每个键,然后使用reduce 构建嵌套属性。

var obj2 = "firstName":"John","lastName":"Green","car.make":"Honda","car.model":"Civic","car.revisions.0.miles":10150,"car.revisions.0.code":"REV01","car.revisions.0.changes":"","car.revisions.1.miles":20021,"car.revisions.1.code":"REV02","car.revisions.1.changes.0.type":"asthetic","car.revisions.1.changes.0.desc":"Left tire cap","car.revisions.1.changes.1.type":"mechanic","car.revisions.1.changes.1.desc":"Engine pressure regulator","visits.0.date":"2015-01-01","visits.0.dealer":"DEAL-001","visits.1.date":"2015-03-01","visits.1.dealer":"DEAL-002"

function unflatten(data) 
  var result = 
  for (var i in data) 
    var keys = i.split('.')
    keys.reduce(function(r, e, j) 
      return r[e] || (r[e] = isNaN(Number(keys[j + 1])) ? (keys.length - 1 == j ? data[i] : ) : [])
    , result)
  
  return result


console.log(unflatten(obj2))

【讨论】:

这个脚本的第 8 行很粗糙,很难弄清楚发生了什么。 r[e] = isNan(... 有什么作用?三元的前面是布尔值,赋值会返回什么吗? @ohsully isNaN(Number(keys[j + 1])) 在从字符串转换为数字时检查下一个键是否为数字。如果它返回 true,则意味着它不是数字,然后您评估另一个条件以查看其最后一个键是否并分配值或空对象。但是如果它返回 false 那么它是一个数字,然后它分配一个数组。 所以如果你有这样的密钥car.revisions.0.miles 前两部分(汽车和修订)不是数字,第三部分是数字,最后一部分不是数字。【参考方案2】:

尝试将问题分解为两个不同的挑战:

    按路径设置值 循环遍历对象并逐个展开键

您可以从一个看起来像这样的setIn 函数开始:

function setIn(path, object, value) 
  let [key, ...keys] = path; 

  if (keys.length === 0) 
    object[key] = value;
   else 
    let nextKey = keys[0];
    object[key] = object[key] || isNaN(nextKey) ?  : [];
    setIn(keys, object[key], value);
  

  return object;

然后将它与 unflatten 函数结合起来,该函数循环遍历每个键运行 setIn 的对象。

function unflatten(flattened) 
  let object = ;

  for (let key in flattened) 
    let path = key.split('.');
    setIn(path, object, flattened[key]);
  

  return object;

当然,已经有一个npm package 可以做到这一点,而且使用_.set from lodash 之类的函数也很容易实现自己的功能。

您不太可能遇到足够长的路径以致最终耗尽堆栈帧,但当然可以使用循环或 trampolines 不递归地实现 setIn

最后,如果您喜欢不可变数据并且您希望使用不修改数据结构的setIn 版本,那么您可以查看Zaphod 中的实现——一个javascript 库用于将本机数据结构视为不可变的。

【讨论】:

【参考方案3】:

您可以遍历拆分的substrings 和一个临时对象,并检查键是否存在,并构建一个新属性并检查下一个属性是否为有限数,然后分配一个数组,否则分配一个对象。最后用最后一个子字符串将值赋给 temp 对象。

function unflatten(obj) 
    var result = , temp, substrings, property, i;
    for (property in obj) 
        substrings = property.split('.');
        temp = result;
        for (i = 0; i < substrings.length - 1; i++) 
            if (!(substrings[i] in temp)) 
                if (isFinite(substrings[i + 1]))  // check if the next key is
                    temp[substrings[i]] = [];      // an index of an array
                 else 
                    temp[substrings[i]] = ;      // or a key of an object
                
            
            temp = temp[substrings[i]];
        
        temp[substrings[substrings.length - 1]] = obj[property];
    
    return result;
;

var obj2 =  "firstName": "John", "lastName": "Green", "car.make": "Honda", "car.model": "Civic", "car.revisions.0.miles": 10150, "car.revisions.0.code": "REV01", "car.revisions.0.changes": "", "car.revisions.1.miles": 20021, "car.revisions.1.code": "REV02", "car.revisions.1.changes.0.type": "asthetic", "car.revisions.1.changes.0.desc": "Left tire cap", "car.revisions.1.changes.1.type": "mechanic", "car.revisions.1.changes.1.desc": "Engine pressure regulator", "visits.0.date": "2015-01-01", "visits.0.dealer": "DEAL-001", "visits.1.date": "2015-03-01", "visits.1.dealer": "DEAL-002" ;

console.log(unflatten(obj2));
.as-console-wrapper  max-height: 100% !important; top: 0; 

一个稍微紧凑的版本可能是这样的

function unflatten(object) 
    var result = ;
    Object.keys(object).forEach(function (k) 
        setValue(result, k, object[k]);
    );
    return result;


function setValue(object, path, value) 
    var way = path.split('.'),
        last = way.pop();

    way.reduce(function (o, k, i, kk) 
        return o[k] = o[k] || (isFinite(i + 1 in kk ? kk[i + 1] : last) ? [] : );
    , object)[last] = value;


var obj2 =  "firstName": "John", "lastName": "Green", "car.make": "Honda", "car.model": "Civic", "car.revisions.0.miles": 10150, "car.revisions.0.code": "REV01", "car.revisions.0.changes": "", "car.revisions.1.miles": 20021, "car.revisions.1.code": "REV02", "car.revisions.1.changes.0.type": "asthetic", "car.revisions.1.changes.0.desc": "Left tire cap", "car.revisions.1.changes.1.type": "mechanic", "car.revisions.1.changes.1.desc": "Engine pressure regulator", "visits.0.date": "2015-01-01", "visits.0.dealer": "DEAL-001", "visits.1.date": "2015-03-01", "visits.1.dealer": "DEAL-002" ;

console.log(unflatten(obj2));
.as-console-wrapper  max-height: 100% !important; top: 0; 

【讨论】:

以上是关于如何将菊花链/点符号中的 JavaScript 对象展开为具有嵌套对象和数组的对象?的主要内容,如果未能解决你的问题,请参考以下文章

菊花链 Python/Django 自定义装饰器

JavaScript中的原型与原型链

编写高性能 JavaSCript 注意点

通过 Javascript 中的承诺链传递状态都有哪些模式? [复制]

如何将 Java Map 转换为基本的 Javascript 对象?

如何将 javascript regexp 中的 Euro € 符号与八进制、十六进制或 unicode 元字符匹配?