高效地为对象数组中的JavaScript/JSON对象键进行重命名/重新映射

26

我有一些结构化的JSON数据,如下所示。假设可以通过JSON.parse()进行交换:

[
    {
        "title": "pineapple",
        "uid": "ab982d34c98f"
    },
    {
        "title": "carrots",
        "uid": "6f12e6ba45ec"
    }
]

我需要它看起来像这样,将title重新映射为name,将uid重新映射为id,并产生以下结果:

[
    {
        "name": "pineapple",
        "id": "ab982d34c98f"
    },
    {
        "name": "carrots",
        "id": "6f12e6ba45ec"
    }
]

最直观的方法是这样的:
str = '[{"title": "pineapple","uid": "ab982d34c98f"},{"title": "carrots", "uid": "6f12e6ba45ec"}]';

var arr = JSON.parse(str);
for (var i = 0; i<arr.length; i++) {
    arr[i].name = arr[i].title;
    arr[i].id = arr[i].uid;
    delete arr[i].title;
    delete arr[i].uid;
}

str = '[{"title": "pineapple","uid": "ab982d34c98f"},{"title": "carrots",      "uid": "6f12e6ba45ec"}]';

var arr = JSON.parse(str);
for (var i = 0; i<arr.length; i++) {
    arr[i].name = arr[i].title;
    arr[i].id = arr[i].uid;
    delete arr[i].title;
    delete arr[i].uid;
}

$('body').append("<pre>"+JSON.stringify(arr, undefined, 4)+"</pre>");
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

...或者使用更复杂的方法 (尽管不一定更高效),比如这个

这一切看起来都很好,但如果数组中有200,000个对象怎么办? 这会导致很多处理开销。

有没有更高效的方法来重新映射键名? 可能无需循环遍历整个对象数组? 如果您的方法更高效,请提供证明/参考资料。


考虑创建一个由{from, to}对组成的数组,然后使用map函数遍历arr - lexasss
请注意,这个问题与JSON无关,除非您想进行字符串处理以更改键。 - Felix Kling
你怎么能在不遍历数组的情况下修改数组中的每个对象呢?这没有意义。为什么你需要重新映射? - Brian Glaz
可以将其视为正则表达式替换的一种比较方式,例如 str = str.replace(/"title":/g, '"name":');。当然,这种方法对对象可能具有的值进行了一定的假设,但它很容易实现,为什么不试试呢? - Felix Kling
可能是更改嵌套JSON结构中的键名的重复问题。 - Gabe
8个回答

31

正如我在评论中提到的那样,如果你能对对象的值做出一些假设,你可以使用正则表达式来替换键,例如:

str = str.replace(/"title":/g, '"name":');

这种方法不太“干净”,但可能更快地完成工作。


如果你必须解析JSON,那么更有结构化的方法是将一个修复器函数传递给JSON.parse,这样你可能可以避免对数组进行额外的解析。不过这可能取决于引擎如何实现JSON.parse(也许它们首先解析整个字符串,然后再使用修复器函数进行第二次解析,在这种情况下,你就无法获得任何优势)。
var arr = JSON.parse(str, function(prop, value) {
   switch(prop) {
     case "title":
        this.name = value;
        return;
     case "uid":
        this.id = value;
        return;
     default:
        return value;
   }
});

使用下面的Node.js脚本进行基准测试,测试3次:

1389822740739: Beginning regex rename test
1389822740761: Regex rename complete
// 22ms, 22ms, 21ms
1389822740762: Beginning parse and remap in for loop test
1389822740831: For loop remap complete
// 69ms, 68ms, 68ms
1389822740831: Beginning reviver function test
1389822740893: Reviver function complete
// 62ms, 61ms, 60ms

看起来在这种情况下,正则表达式是最有效的,但是尝试使用正则表达式解析JSON时要小心


测试脚本,加载100,230行原始数据JSON样例:

fs = require('fs');
fs.readFile('test.json', 'utf8', function (err, data) {
    if (err) {
        return console.log(err);
    }
    console.log(new Date().getTime() + ": Beginning regex rename test");
    var str = data.replace(/"title":/g, '"name":');
    str = str.replace(/"uid":/g, '"id":');
    JSON.parse(str);
    console.log(new Date().getTime() + ": Regex rename complete");
    console.log(new Date().getTime() + ": Beginning parse and remap in for loop test");
    var arr = JSON.parse(data);
    for (var i = 0; i < arr.length; i++) {
        arr[i].name = arr[i].title;
        arr[i].id = arr[i].uid;
        delete arr[i].title;
        delete arr[i].uid;
    }
    console.log(new Date().getTime() + ": For loop remap complete");
    console.log(new Date().getTime() + ": Beginning reviver function test");
    var arr = JSON.parse(data, function (prop, value) {
        switch (prop) {
            case "title":
                this.name = value;
                return;
            case "uid":
                this.id = value;
                return;
            default:
                return value;
        }
    });
    console.log(new Date().getTime() + ": Reviver function complete");
});

@remus:是啊,那将会很酷!抱歉,我现在没时间自己做这些测试。 - Felix Kling
1
好的,已更新。希望你不介意我这样劫持了它,但现在有大量信息可用了。 - brandonscript
太棒了!很高兴看到复活功能至少快了一点(虽然不是很多)。是的,你必须小心正则表达式,并确保你得到的输入是正确的。 - Felix Kling
是的,实际上我认为那就是我最终会做的事情。 - brandonscript
1
@Ankuli:没错。就像我说的,JSON.parse 期望传入一个字符串,而不是一个对象。 - Felix Kling
显示剩余6条评论

17

很久以前就问过这个问题了,从那时起,我已经习惯使用Array.prototype.map()来完成任务,更多是为了代码的稳定性和清晰度,而不是为了性能。虽然它肯定不是最有效率的,但它看起来很棒:

var repl = orig.map(function(obj) {
    return {
        name: obj.title,
        id: obj.uid
    }
})

如果您需要一个更加灵活的(并且兼容ES6的)函数,请尝试:

let replaceKeyInObjectArray = (a, r) => a.map(o => 
    Object.keys(o).map((key) => ({ [r[key] || key] : o[key] })
).reduce((a, b) => Object.assign({}, a, b)))

需要翻译的内容:

e.g.

const arr = [{ abc: 1, def: 40, xyz: 50 }, { abc: 1, def: 40, xyz: 50 }, { abc: 1, def: 40, xyz: 50 }]
const replaceMap = { "abc": "yyj" }

replaceKeyInObjectArray(arr, replaceMap)

/*
[
    {
        "yyj": 1,
        "def": 40,
        "xyz": 50
    },
    {
        "yyj": 1,
        "def": 40,
        "xyz": 50
    },
    {
        "yyj": 1,
        "def": 40,
        "xyz": 50
    }
]
*/

在编程中,使用map来引发副作用是非常不寻常的,你应该使用forEach来处理这种情况。但是,由于map返回一个新数组,你应该在回调函数中进行转换并将该数组保存到repl中。 - Stefano

10

这里提供另一种观点,即使用map()以增强代码的清晰度(而非性能)。

var newItems = items.map(item => ({
    name: item.title,
    id: item.uid
}));

这里使用了ES6箭头函数和当函数只有一个参数以及函数体内仅有一条语句时所允许的简写语法。

根据你对于不同编程语言中lambda表达式的理解程度,这种形式可能会或不会与你产生共鸣。

使用箭头函数简写语法返回一个对象字面量时需要小心,不要忘记在对象字面量周围加上额外的括号!


2
使用 ES6:
const renameFieldInArrayOfObjects = (arr, oldField, newField) => {
  return arr.map(s => {
    return Object.keys(s).reduce((prev, next) => {
      if(next === oldField) { 
        prev[newField] = s[next]
      } else { 
        prev[next] = s[next] 
      }
      return prev
    }, {})
  })
}

使用 ES7:

const renameFieldInArrayOfObjects = (arr, oldField, newField) => {
  return arr.map(s => {
    return Object.keys(s).reduce((prev, next) => {
      return next === oldField
        ? {...prev, [newField]: s[next]}
        : {...prev, [next]: s[next]}
    }, {})
  })
}

喜欢它 - 我认为使用ES6甚至可以进一步减少它? - brandonscript
@brandonscript,我不知道如何使用三元运算符来同时设置属性和返回对象。 - ssomnoremac
@brandonscript,这真的很有效,谢谢你让我重新思考它。这个例子中包含了很多ES6语法。 - ssomnoremac
啊,对象支持仅在ES7(提议中)可用。 - brandonscript

2
您可以使用一个名为 node-data-transform 的npm包。
您的数据:
const data = [
  {
    title: 'pineapple',
    uid: 'ab982d34c98f',
  },
  {
    title: 'carrots',
    uid: '6f12e6ba45ec',
  },
];

您的映射:
const map = {
  item: {
    name: 'title',
    id: 'uid',
  },
};

使用该软件包:
const DataTransform = require("node-json-transform").DataTransform;
const dataTransform = DataTransform(data, map);
const result = dataTransform.transform();
console.log(result);

结果:
[
  {
    name: 'pineapple',
    id: 'ab982d34c98f'
  },
  {
    name: 'carrots',
    id: '6f12e6ba45ec'
  }
]

也许这不是最佳的性能方式,但它非常优雅。

2
如果你想让它更具有复用性,也许这是一个不错的方法。

function rekey(arr, lookup) {
 for (var i = 0; i < arr.length; i++) {
  var obj = arr[i];
  for (var fromKey in lookup) {
   var toKey = lookup[fromKey];
   var value = obj[fromKey];
   if (value) {
    obj[toKey] = value;
    delete obj[fromKey];
   }
  }
 }
 return arr;
}

var arr = [{ apple: 'bar' }, { apple: 'foo' }];
var converted = rekey(arr, { apple: 'kung' });
console.log(converted);


1
您的代码片段在我的电脑上无法运行。我正在使用Chrome和Mac系统。 - grepit
你确定吗?它是 console.log,它会在开发者工具中记录在浏览器控制台中。在我的电脑上,使用Mac上的Chrome似乎可以正常工作。 - luwes
我将 if (value) { 更改为 if (typeof value !== "undefined") {,因为它跳过了一些键。 - Karl_S
我还添加了一个选项来反转键,因为我需要将数据返回到原始状态。function rekey(arr, lookup, reverse) { //如果未传递,则设置默认值为false if (reverse === undefined) { reverse = false; } for (var i = 0; i < arr.length; i++) { var obj = arr[i]; for (var fromKey in lookup) { var toKey = lookup[fromKey]; var value = obj[fromKey]; if (typeof value !== 'undefined' && !reverse) {... - Karl_S

1
var jsonObj = [/*sample array in question*/   ]

根据下面讨论的不同基准,最快的解决方案是本地的:

var arr = [];
for(var i = 0, len = jsonObj .length; i < len; i++) {
  arr.push( {"name": jsonObj[i].title, "id" : jsonObj[i].uid});
}

我认为如果不使用框架,这将是第二个选项:

var arr = []
jsonObj.forEach(function(item) { arr.push({"name": item.title, "id" : item.uid }); });

使用本地和非本地函数之间一直存在争议。如果我没记错的话,lodash 认为它们比 underscore 更快,因为它们使用非本机函数进行关键操作。

然而,不同的浏览器有时会产生非常不同的结果。我总是寻找最佳平均值。

您可以查看以下基准测试:

http://jsperf.com/lo-dash-v1-1-1-vs-underscore-v1-4-4/8


为什么这样会更有效率?请详细说明。 - Felix Kling
3
根据你的基准测试,Array.map 在 Chrome 和 Safari 中实际上比使用 for 循环更慢。这并没有帮助太多。此外,我最终会在 Node.js 中构建这个程序,并且将依赖于 V8 引擎。 - brandonscript
@remus 如果你的问题与NodeJS有关,那就是另一回事了,这也是我提到跨浏览器平均值的原因。 - Dalorzo
感谢大家宝贵的评论,我根据我们的讨论更新了答案。 - Dalorzo
@Bergi:我明白了。我总是忘记有些操作是被优化的(或者不被优化)。谢谢! - Felix Kling
显示剩余7条评论

0
function replaceElem(value, replace, str) {
            while (str.indexOf(value) > -1) {
                str = str.replace(value, replace);
            }
            return str;
        }

从主函数调用此函数

var value = "tittle";
var replace = "name";
replaceElem(value, replace, str);

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