如何将一个用点符号表示法连接的JavaScript对象展开为嵌套对象和数组的对象?

16

我想要展开这样的一个对象...

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;
};

为了嵌套对象和数组,我很快就开始不必要地重复编写代码。这绝对需要递归。有任何想法吗?

编辑:我还在另一个问题中提出了相反的扁平化问题。


2
我在这里有一种强烈的似曾相识的感觉。我敢肯定我昨天已经读过这个问题了。-- 编辑:好吧,这与昨天的问题相反。 - John Weisz
上一个问题是关于“flatten”的,这个问题是关于“unflatten”的。感觉像一些家庭作业。 - Andrey
6个回答

26
你可以首先使用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
@ohsully isNaN(Number(keys[j + 1])) 检查下一个键是否可以从字符串转换为数字。如果返回 true,则表示它不是数字,然后您需要评估另一个条件来查看它是否是最后一个键,然后分配值或空对象。但如果返回 false,则它是一个数字,然后将其分配给数组。 - Nenad Vracar
所以,如果您有像car.revisions.0.miles这样的键,前两部分(car和revisions)不是数字,第三部分是数字,最后一部分不是数字。 - Nenad Vracar

6
尝试将问题分解为两个不同的挑战:
  1. 通过路径设置一个值
  2. 循环遍历对象并逐个取消扁平化的键
你可以从编写一个类似下面这样的`setIn`函数开始:
```javascript function setIn(obj, path, value) { // your code here } ```
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包 可以做到这一点,而且使用像 lodash的_.set函数 这样的函数也很容易实现自己的功能。
你不太可能遇到足够长的路径而耗尽堆栈帧,但当然可以使用循环或 trampolines 来实现无递归的 setIn
最后,如果不可变数据是你的事情,并且你想使用不修改数据结构的版本的 setIn,那么可以查看 Zaphod 中的实现——这是一个用于将原生数据结构视为不可变的 JavaScript 库。

你有一个错误。应该是 object[key] = object[key] || (isNaN(nextKey) ? {} : []); 或者简单地写作 object[key] ??= isNaN(nextKey) ? {} : []; - ruohola

5
你可以使用lodash库中的keysset函数来构建更简洁的代码。
这个想法是,你可以使用.set方法转换对象中的每个条目,并将其合并到全局对象中。

const messages = {
        "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"
};

const unflatten = (flattedObject) => {
    let result = {}
    _.keys(flattedObject).forEach(function (key, value){    
        _.set(result, key, flattedObject[key])        
    })
    return result
}

console.log(unflatten(messages))
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.min.js"></script>


0
你可以遍历被分割的子字符串和一个临时对象,检查键是否存在并构建一个新属性,如果下一个属性是有限数字,则分配一个数组,否则分配一个对象。最后,使用最后一个子字符串将值分配给临时对象。

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; }


0

const data = {
  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',
};

const objKeys = Object.keys(data);

function assign(obj, keys, val) {
  const lastKey = keys.pop();
  const lastObj = keys.reduce((obj, key) => (obj[key] = obj[key] || {}), obj);
  lastObj[lastKey] = val;
}

const final = objKeys.reduce((acc, cv) => {
  if (!cv.includes('.')) {
    acc[cv] = data[cv];
  }

  const keys = cv.split('.');
  assign(acc, keys, data[cv]);

  return acc;
}, {});

console.log({ final });


0
type tsAppendableObj = { [key: string]: any }
const test = {
    r_0_0_1: {
        component: 'header_hosted_by',
        langs: 'header_hosted_by',
    },
    r_0_0_2: {
        component: 'header_lang',
        langs: '',
    },
    r_0_0_2_0: {
        component: 'header_lang_en',
        langs: 'header_lang_en',
    },
    r_0_0_2_1: {
        component: 'header_lang_fr',
        langs: 'header_lang_fr',
    },
    r_0: {
        component: 'title',
        langs: 'info_page_title',
    },
    r_0_0: {
        component: 'header',
        langs: 'api_version',
    },
    r_0_0_0: {
        component: 'header_date',
        langs: 'header_date',
    },
}

function unflatten2(data: tsAppendableObj, separator: string) {
    let result = <tsAppendableObj>{}
    let temp = <tsAppendableObj>{}
    let temp2 = <tsAppendableObj>{}
    let levels = <tsAppendableObj>{}
    let property
    let i

    for (property in data) {
        levels = property.split(separator)
        let gg = levels[0]
        temp = result
        for (i = 1; i < levels.length; i++) {
            gg = gg + separator + levels[i]
            if (!temp[gg]) {
                temp[gg] = {
                    main: {},
                    items: {},
                }
            }
            if (i === levels.length - 1) temp[gg].main = data[property]
            temp = temp[gg].items
        }
    }
    return result
}

console.log(unflatten2(test, '_'))

如果在无序列表中工作,它会提供更友好的主/项结构,并带有完整的键,因此您可以更多或少地递归迭代它...可以改进... - Dexterial

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