排序对象属性和 JSON.stringify

IT技术 javascript json
2021-01-21 07:20:48

我的应用程序有大量对象,我将它们字符串化并将它们保存到磁盘。不幸的是,当数组中的对象被操作,有时被替换时,对象上的属性以不同的顺序列出(它们的创建顺序?)。当我对数组执行 JSON.stringify() 并保存它时,差异显示以不同顺序列出的属性,这在尝试使用差异和合并工具进一步合并数据时很烦人。

理想情况下,我想在执行字符串化之前或作为字符串化操作的一部分按字母顺序对对象的属性进行排序。在许多地方都有用于操作数组对象的代码,并且更改这些代码以始终以明确的顺序创建属性将是困难的。

建议将是最受欢迎的!

一个浓缩的例子:

obj = {}; obj.name="X"; obj.os="linux";
JSON.stringify(obj);
obj = {}; obj.os="linux"; obj.name="X";
JSON.stringify(obj);

这两个 stringify 调用的输出不同,并显示在我的数据差异中,但我的应用程序并不关心属性的顺序。对象以多种方式和位置构建。

6个回答

更简单、现代且当前浏览器支持的方法很简单:

JSON.stringify(sortMyObj, Object.keys(sortMyObj).sort());

但是,此方法会删除任何未引用且不适用于数组中的对象的嵌套对象。如果您想要这样的输出,您还需要展平排序对象:

{"a":{"h":4,"z":3},"b":2,"c":1}

你可以这样做:

var flattenObject = function(ob) {
    var toReturn = {};
    
    for (var i in ob) {
        if (!ob.hasOwnProperty(i)) continue;
        
        if ((typeof ob[i]) == 'object') {
            var flatObject = flattenObject(ob[i]);
            for (var x in flatObject) {
                if (!flatObject.hasOwnProperty(x)) continue;
                
                toReturn[i + '.' + x] = flatObject[x];
            }
        } else {
            toReturn[i] = ob[i];
        }
    }
    return toReturn;
};
var myFlattenedObj = flattenObject(sortMyObj);
JSON.stringify(myFlattenedObj, Object.keys(myFlattenedObj).sort());

要使用您可以自己调整的东西以编程方式执行此操作,您需要将对象属性名称推送到一个数组中,然后按字母顺序对数组进行排序并遍历该数组(按正确的顺序)并从对象中选择每个值那个命令。“hasOwnProperty”也被检查,所以你肯定只有对象自己的属性。下面是一个例子:

var obj = {"a":1,"b":2,"c":3};

function iterateObjectAlphabetically(obj, callback) {
    var arr = [],
        i;
    
    for (i in obj) {
        if (obj.hasOwnProperty(i)) {
            arr.push(i);
        }
    }

    arr.sort();
    
    for (i = 0; i < arr.length; i++) {
        var key = obj[arr[i]];
        //console.log( obj[arr[i]] ); //here is the sorted value
        //do what you want with the object property
        if (callback) {
            // callback returns arguments for value, key and original object
            callback(obj[arr[i]], arr[i], obj);
        }
    }
}

iterateObjectAlphabetically(obj, function(val, key, obj) {
    //do something here
});

同样,这应该保证您按字母顺序迭代。

最后,以最简单的方式更进一步,该库将递归地允许您对传递给它的任何 JSON 进行排序:https : //www.npmjs.com/package/json-stable-stringify

var stringify = require('json-stable-stringify');
var obj = { c: 8, b: [{z:6,y:5,x:4},7], a: 3 };
console.log(stringify(obj));

输出

{"a":3,"b":[{"x":4,"y":5,"z":6},7],"c":8}
这段代码有一个错误。您不能obj[i]在 for 循环中引用,因为它i是一个整数,并且属性名称不一定(因此是这个问题)。应该是obj[arr[i]]
2021-04-03 07:20:48
回调不是异步行为独有的。
2021-04-07 07:20:48
啊,我必须把那一点信息放在答案的下一句话中!等一下...
2021-04-08 07:20:48
@Johann 通过在这里使用回调,他变成iterateObjectAlphabetically了一个可重用的函数。如果没有回调,“有效载荷代码”就必须在函数内部。我认为这是一个非常优雅的解决方案,并且在许多库和 JS 核心本身中使用。例如Array.forEach
2021-04-09 07:20:48
这正是我试图避免的 :) 但是谢谢,这似乎是正确的(如果很重)解决方案。
2021-04-13 07:20:48

我不明白为什么需要当前最佳答案的复杂性,以递归方式获取所有密钥。除非需要完美的性能,在我看来,我们可以只调用JSON.stringify()两次,第一次获取所有密钥,第二次真正完成工作。这样,所有的递归复杂性都由 处理stringify,我们知道它知道它的内容,以及如何处理每个对象类型:

function JSONstringifyOrder( obj, space )
{
    var allKeys = [];
    var seen = {};
    JSON.stringify(obj, function (key, value) {
        if (!(key in seen)) {
            allKeys.push(key);
            seen[key] = null;
        }
        return value;
    });
    allKeys.sort();
    return JSON.stringify(obj, allKeys, space);
}
@Jor 谢谢,这对我有用。我尝试过的其他片段以某种方式失败了。
2021-03-19 07:20:48
不,关键是它Object.keys()不是递归的,所以如果你有一个像这样的对象{a: "A", b: {c: "C"} }并且你在它上面调用 Object.keys,你只会得到键ab,而不是cJSON.stringify了解有关 JSON 序列化过程处理的每个对象类型的递归的一切,无论是类对象还是数组(并且它已经实现了识别两者的逻辑),因此它应该为我们正确处理所有内容。
2021-03-21 07:20:48
这很有趣,但您实际上“欺骗”了使用 stringify 将所有键映射到数组。通过 Object.keys 可以更容易、更好、更安全地获得该数组
2021-03-24 07:20:48
喜欢这个解决方案,看起来非常优雅且安全。
2021-04-01 07:20:48
聪明的!re perf:我相信这是 O(N^2), N=number 对象中的键,因为对对象中的每个键线性检查排序的键。因此,在 GHz 机器上,性能将开始成为 N > 100K 左右的明显问题。对于 N > 100 万,可能会削弱性能。参考:tc39.es/ecma262/#sec-json.stringify
2021-04-11 07:20:48

我认为,如果您可以控制 JSON 生成(听起来像您一样),那么就您的目的而言,这可能是一个很好的解决方案:json-stable-stringify

从项目网站:

确定性 JSON.stringify() 使用自定义排序从字符串化结果中获取确定性哈希

如果生成的 JSON 是确定性的,您应该能够轻松地对其进行区分/合并。

ES6 我猜破坏了它能够按照指定对对象(非数组)进行排序的能力。我需要这个用于用户编辑。即使顺序对计算机也无所谓。它对编辑它的用户有用。值得一试。
2021-03-15 07:20:48
请注意,该 repo 状态不佳:3 年未更新,并且有未解决的问题和拉取请求。我认为最好查看一些较新的答案。
2021-03-26 07:20:48
除了@Tom 所说的,我还要指出这个项目没有运行时依赖项。这意味着如果此 repo 中没有安全/维护问题,那么它可能非常稳定且使用安全。另外,我刚刚用 ES6 测试了它,它似乎工作正常(至少在 Firefox 中)。
2021-03-27 07:20:48
@Tom 另请注意,该存储库每周 (!) 下载量达 500 万次。换句话说,它正在被积极使用。拥有“未解决”的未解决问题和/或拉取请求并没有多大意义。只是作者不关心这些问题或没有时间等。并非每个库都需要每几个月更新一次。事实上,恕我直言,最好的库很少更新。当某事完成时,它就完成了。我并不是说这个项目没有真正的问题,但如果是这样,请指出这些。顺便说一句,我没有参与这个库。
2021-03-28 07:20:48

您可以将属性名称的排序数组作为 的第二个参数传递JSON.stringify()

JSON.stringify(obj, Object.keys(obj).sort())
Object.keys()不是递归的。这不适用于包含嵌套数组或对象的任何对象。
2021-03-25 07:20:48
@richremer 是的,在ECMAScript 标准中, 的第二个参数JSON.stringify()被调用replacer并且可以是一个数组。它用于构建一个PropertyList,然后用于SerializeJSONObject()按该顺序附加属性。自 ECMAScript 5 起就记录了这一点。
2021-03-29 07:20:48
这种行为是否有任何形式的书面保证?
2021-04-04 07:20:48
请注意,如果 obj 中有嵌套对象,则嵌套键也必须出现在第二个参数中;否则,他们将被丢弃!反例:> JSON.stringify({a: {c: 1, b: 2}}, ['a']) '{"a":{}}' 构建所有相关键列表的一种(hacky)方法是使用 JSON.stringify 遍历对象: function orderedStringify(obj) { const allKeys = []; JSON.stringify(obj, (k, v) => { allKeys.push(k); return v; }); return JSON.stringify(obj, allKeys.sort()); } 示例:> orderedStringify({a: {c: 1, b: 2}}) '{"a":{"b":2,"c":1}}'
2021-04-08 07:20:48

2018-7-24 更新:

此版本对嵌套对象进行排序并支持数组:

function sortObjByKey(value) {
  return (typeof value === 'object') ?
    (Array.isArray(value) ?
      value.map(sortObjByKey) :
      Object.keys(value).sort().reduce(
        (o, key) => {
          const v = value[key];
          o[key] = sortObjByKey(v);
          return o;
        }, {})
    ) :
    value;
}


function orderedJsonStringify(obj) {
  return JSON.stringify(sortObjByKey(obj));
}

测试用例:

  describe('orderedJsonStringify', () => {
    it('make properties in order', () => {
      const obj = {
        name: 'foo',
        arr: [
          { x: 1, y: 2 },
          { y: 4, x: 3 },
        ],
        value: { y: 2, x: 1, },
      };
      expect(orderedJsonStringify(obj))
        .to.equal('{"arr":[{"x":1,"y":2},{"x":3,"y":4}],"name":"foo","value":{"x":1,"y":2}}');
    });

    it('support array', () => {
      const obj = [
        { x: 1, y: 2 },
        { y: 4, x: 3 },
      ];
      expect(orderedJsonStringify(obj))
        .to.equal('[{"x":1,"y":2},{"x":3,"y":4}]');
    });

  });

弃用的答案:

ES2016 中的简洁版本。归功于 @codename ,来自https://stackoverflow.com/a/29622653/94148

function orderedJsonStringify(o) {
  return JSON.stringify(Object.keys(o).sort().reduce((r, k) => (r[k] = o[k], r), {}));
}
语法不太对。应该 -function orderedJsonStringify(o) { return JSON.stringify(Object.keys(o).sort().reduce((r, k) => (r[k] = o[k], r), {})); }
2021-03-18 07:20:48
这似乎没有sortObjByKey()检查循环引用,所以要小心,它可能会根据输入数据挂在无限循环中。示例:var objA = {}; var objB = {objA: objA}; objA.objB = objB;-> sortObjByKey(objA);->VM59:6 Uncaught RangeError: Maximum call stack size exceeded
2021-03-22 07:20:48
目前,这已经解决了我的问题,即 C# DataContractJsonSerializer 和“__type”未在 json 字符串中首先列出。谢谢。
2021-03-23 07:20:48
请注意,这与 Christian 的回答一样,对嵌套对象有不当行为。
2021-03-23 07:20:48
sortObjPropertiesByKey 未定义。您的意思是使用 sortObjByKey 吗?
2021-04-13 07:20:48