Сортировать свойства объекта и JSON.stringify

Мое приложение имеет большой массив объектов, которые я преобразую в строку и сохраняю на диск. К сожалению, когда объектами в массиве манипулируют, а иногда и заменяют, свойства объектов перечисляются в разных порядках (порядок их создания?). Когда я выполняю JSON.stringify() для массива и сохраняю его, diff показывает свойства, перечисленные в разных порядках, что раздражает, когда я пытаюсь объединить данные с помощью diff и инструментов слияния.

В идеале я хотел бы отсортировать свойства объектов в алфавитном порядке перед выполнением stringify или как часть операции stringify. Существует код для манипулирования объектами массива во многих местах, и изменить их так, чтобы всегда создавать свойства в явном порядке, было бы сложно.

Предложения будут приветствоваться!

Сокращенный пример:

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

Вывод этих двух вызовов stringify различен и отображается в разных моих данных, но мое приложение не заботится об упорядочении свойств. Объекты создаются разными способами и в разных местах.

26 ответов

Решение

Более простой, современный и в настоящее время поддерживаемый браузером подход заключается в следующем:

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

JSON.stringify(sortMyObj, Object.keys(flattenObject(sortMyObj)).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}

Я не понимаю, почему сложность текущих лучших ответов необходима для рекурсивного получения всех ключей (но я не могу их комментировать из-за ограничений репутации...). Если не требуется идеальная производительность, мне кажется, что мы можем просто позвонить JSON.stringify() дважды, первый раз, чтобы получить все ключи, и второй раз, чтобы действительно сделать работу. Таким образом, вся сложность рекурсии обрабатывается stringifyи мы знаем, что он знает свое дело и как обрабатывать каждый тип объекта:

function JSONstringifyOrder( obj, space )
{
    var allKeys = [];
    JSON.stringify( obj, function( key, value ){ allKeys.push( key ); return value; } )
    allKeys.sort();
    return JSON.stringify( obj, allKeys, space );
}

Я думаю, что если вы контролируете поколение JSON (и звучит так, как вы), то для ваших целей это может быть хорошим решением: https://npmjs.org/package/json-stable-stringify

С сайта проекта:

детерминированный JSON.stringify() с пользовательской сортировкой для получения детерминированных хешей из строковых результатов

Если созданный JSON является детерминированным, вы сможете легко его объединить.

https://gist.github.com/davidfurlong/463a83a33b70a3b6618e97ec9679e490

const replacer = (key, value) =>
    value instanceof Object && !(value instanceof Array) ? 
        Object.keys(value)
        .sort()
        .reduce((sorted, key) => {
            sorted[key] = value[key];
            return sorted 
        }, {}) :
        value;

Вы можете передать отсортированный массив имен свойств в качестве второго аргумента JSON.stringify():

JSON.stringify(obj, Object.keys(obj).sort())

Обновление 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, с /questions/13872306/sortirovat-obekt-javascript-po-klyuchu/13872314#13872314

function orderedJsonStringify(o) {
  return JSON.stringify(Object.keys(o).sort().reduce((r, k) => (r[k] = o[k], r), {}));
}

Это так же, как ответ Сатпала Сингха

function stringifyJSON(obj){
    keys = [];
    if(obj){
        for(var key in obj){
            keys.push(key);
        }
    }
    keys.sort();
    var tObj = {};
    var key;
    for(var index in keys){
        key = keys[index];
        tObj[ key ] = obj[ key ];
    }
    return JSON.stringify(tObj);
}

obj1 = {}; obj1.os="linux"; obj1.name="X";
stringifyJSON(obj1); //returns "{"name":"X","os":"linux"}"

obj2 = {}; obj2.name="X"; obj2.os="linux";
stringifyJSON(obj2); //returns "{"name":"X","os":"linux"}"

Рекурсивный и упрощенный ответ:

function sortObject(obj) {
    if(typeof obj !== 'object')
        return obj
    var temp = {};
    var keys = [];
    for(var key in obj)
        keys.push(key);
    keys.sort();
    for(var index in keys)
        temp[keys[index]] = sortObject(obj[keys[index]]);       
    return temp;
}

var str = JSON.stringify(sortObject(obj), undefined, 4);

Вы можете отсортировать объект по имени свойства в EcmaScript 2015

function sortObjectByPropertyName(obj) {
    return Object.keys(obj).sort().reduce((c, d) => (c[d] = obj[d], c), {});
}

Я просто переписал один из упомянутых примеров, чтобы использовать его в stringify

      const stringifySort = (key, value) => {
    if (!value || typeof value !== 'object' || Array.isArray(value)) return value;
    return Object.keys(value).sort().reduce((obj, key) => (obj[key]=value[key], obj), {});
};

JSON.stringify({name:"X", os:"linux"}, stringifySort);

Вы можете добавить кастом toJSON функция вашего объекта, который вы можете использовать для настройки вывода. Внутри функции добавление текущих свойств к новому объекту в определенном порядке должно сохранять этот порядок при строковом преобразовании.

Посмотреть здесь:

https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/JSON/stringify

Нет встроенного метода для управления порядком, потому что данные JSON предназначены для доступа к ключам.

Вот jsfiddle с небольшим примером:

http://jsfiddle.net/Eq2Yw/

Попробуйте закомментировать toJSON функция - порядок свойств обратный. Помните, что это может зависеть от браузера, т. Е. В спецификации заказ официально не поддерживается. Он работает в текущей версии Firefox, но если вы хотите 100% надежное решение, вам, возможно, придется написать свою собственную функцию строкового преобразователя.

Редактировать:

Также посмотрите этот SO вопрос, касающийся недетерминированного вывода stringify, особенно деталей Даффа о различиях в браузере:

Как детерминистически проверить, что объект JSON не был изменен?

Дополнительное решение, которое работает и для вложенных объектов:

Я взял ответ от @Jason Parham и сделал некоторые улучшения

function sortObject(obj, arraySorter) {
    if(typeof obj !== 'object')
        return obj
    if (Array.isArray(obj)) {
        if (arraySorter) {
            obj.sort(arraySorter);
        }
        for (var i = 0; i < obj.length; i++) {
            obj[i] = sortObject(obj[i], arraySorter);
        }
        return obj;
    }
    var temp = {};
    var keys = [];
    for(var key in obj)
        keys.push(key);
    keys.sort();
    for(var index in keys)
        temp[keys[index]] = sortObject(obj[keys[index]], arraySorter);       
    return temp;
}

Это устраняет проблему преобразования массивов в объекты, а также позволяет определить порядок сортировки массивов.

Пример:

var data = { content: [{id: 3}, {id: 1}, {id: 2}] };
sortObject(data, (i1, i2) => i1.id - i2.id)

выход:

{content:[{id:1},{id:2},{id:3}]}

Принятый ответ по какой-то причине не работает для вложенных объектов. Это привело меня к написанию собственного кода. Поскольку я пишу это в конце 2019 года, на этом языке доступно еще несколько вариантов.

Обновление: я считаю, что ответ Дэвида Ферлонга является предпочтительным подходом к моей предыдущей попытке, и я проиграл это. Mine полагается на поддержку Object.entries(...), поэтому нет поддержки Internet Explorer.

function normalize(sortingFunction) {
  return function(key, value) {
    if (typeof value === 'object' && !Array.isArray(value)) {
      return Object
        .entries(value)
        .sort(sortingFunction || undefined)
        .reduce((acc, entry) => {
          acc[entry[0]] = entry[1];
          return acc;
        }, {});
    }
    return value;
  }
}

JSON.stringify(obj, normalize(), 2);

-

СОХРАНЕНИЕ СТАРЫХ ВЕРСИЙ ДЛЯ ИСТОРИЧЕСКОЙ СПРАВКИ

Я обнаружил, что простой плоский массив всех ключей в объекте будет работать. Практически во всех браузерах (кроме Edge или Internet Explorer, как и ожидалось) и Node 12+ теперь есть довольно короткое решение, когда доступен Array.prototype.flatMap(...). (Эквивалент lodash тоже подойдет.) Я тестировал только в Safari, Chrome и Firefox, но не вижу причин, по которым он не работал бы где-либо еще, поддерживающем flatMap и стандартный JSON.stringify (...).

function flattenEntries([key, value]) {
  return (typeof value !== 'object')
    ? [ [ key, value ] ]
    : [ [ key, value ], ...Object.entries(value).flatMap(flattenEntries) ];
}

function sortedStringify(obj, sorter, indent = 2) {
  const allEntries = Object.entries(obj).flatMap(flattenEntries);
  const sorted = allEntries.sort(sorter || undefined).map(entry => entry[0]);
  return JSON.stringify(obj, sorted, indent);
}

Благодаря этому вы можете выполнить строковую обработку без сторонних зависимостей и даже передать свой собственный алгоритм сортировки, который сортирует пары записей ключ-значение, так что вы можете сортировать по ключу, полезной нагрузке или их комбинации. Работает для вложенных объектов, массивов и любой смеси простых старых типов данных.

const obj = {
  "c": {
    "z": 4,
    "x": 3,
    "y": [
      2048,
      1999,
      {
        "x": false,
        "g": "help",
        "f": 5
      }
    ]
  },
  "a": 2,
  "b": 1
};

console.log(sortedStringify(obj, null, 2));

Печать:

{
  "a": 2,
  "b": 1,
  "c": {
    "x": 3,
    "y": [
      2048,
      1999,
      {
        "f": 5,
        "g": "help",
        "x": false
      }
    ],
    "z": 4
  }
}

Если вам необходима совместимость со старыми механизмами JavaScript, вы можете использовать эти немного более подробные версии, имитирующие поведение flatMap. Клиент должен поддерживать как минимум ES5, поэтому нет Internet Explorer 8 или ниже.

Они вернут тот же результат, что и выше.

function flattenEntries([key, value]) {
  if (typeof value !== 'object') {
    return [ [ key, value ] ];
  }
  const nestedEntries = Object
    .entries(value)
    .map(flattenEntries)
    .reduce((acc, arr) => acc.concat(arr), []);
  nestedEntries.unshift([ key, value ]);
  return nestedEntries;
}

function sortedStringify(obj, sorter, indent = 2) {
  const sortedKeys = Object
    .entries(obj)
    .map(flattenEntries)
    .reduce((acc, arr) => acc.concat(arr), [])
    .sort(sorter || undefined)
    .map(entry => entry[0]);
  return JSON.stringify(obj, sortedKeys, indent);
}

В конце концов, ему нужен массив, который кэширует все ключи во вложенном объекте (в противном случае он будет пропускать некэшированные ключи). Самый старый ответ просто неверен, потому что второй аргумент не заботится о точечной нотации. Итак, ответ (с помощью Set) становится.

function stableStringify (obj) {
  const keys = new Set()
  const getAndSortKeys = (a) => {
    if (a) {
      if (typeof a === 'object' && a.toString() === '[object Object]') {
        Object.keys(a).map((k) => {
          keys.add(k)
          getAndSortKeys(a[k])
        })
      } else if (Array.isArray(a)) {
        a.map((el) => getAndSortKeys(el))
      }
    }
  }
  getAndSortKeys(obj)
  return JSON.stringify(obj, Array.from(keys).sort())
}

Работает с lodash, вложенными объектами, любым значением атрибута объекта:

function sort(myObj) {
  var sortedObj = {};
  Object.keys(myObj).sort().forEach(key => {
    sortedObj[key] = _.isPlainObject(myObj[key]) ? sort(myObj[key]) : myObj[key]
  })
  return sortedObj;
}
JSON.stringify(sort(yourObj), null, 2)

В зависимости от поведения Chrome и Node первый ключ, назначенный объекту, выводится первым JSON.stringify,

Вот подход клонирования... клонируйте объект перед преобразованием в json:

function sort(o: any): any {
    if (null === o) return o;
    if (undefined === o) return o;
    if (typeof o !== "object") return o;
    if (Array.isArray(o)) {
        return o.map((item) => sort(item));
    }
    const keys = Object.keys(o).sort();
    const result = <any>{};
    keys.forEach((k) => (result[k] = sort(o[k])));
    return result;
}

Если он очень новый, но, похоже, отлично работает с файлами package.json.

До того, как я нашел такие библиотеки, как fast-json-stable-stringify (сам не тестировал их в продакшене), я делал это следующим образом:

      import { flatten } from "flat";
import { set } from 'lodash/fp';

const sortJson = (jsonString) => {
  const object = JSON.parse(jsonString);
  const flatObject = flatten(object);
  const propsSorted = Object.entries(flatObject).map(([key, value]) => ({ key, value })).sort((a, b) => a.key.localeCompare(b.key));
  const objectSorted = propsSorted.reduce((object, { key, value }) => set(key, value, object), {});
  return JSON.stringify(objectSorted);
};


const originalJson = JSON.stringify({ c: { z: 3, x: 1, y: 2 }, a: true, b: [ 'a', 'b', 'c' ] });
console.log(sortJson(originalJson)); // {"a":true,"b":["a","b","c"],"c":{"x":1,"y":2,"z":3}} 

Удивлен, что никто не упомянул о Лодаше isEqual функция.

Выполняет глубокое сравнение двух значений, чтобы определить, эквивалентны ли они.

Примечание. Этот метод поддерживает сравнение массивов, буферов массивов, логических значений, объектов даты, объектов ошибок, карт, чисел, объектов объектов, регулярных выражений, наборов, строк, символов и типизированных массивов. Объектные объекты сравниваются по их собственным, а не унаследованным, перечисляемым свойствам. Функции и узлы DOM сравниваются на строгое равенство, т.е. ===.

https://lodash.com/docs/4.17.11

С исходной проблемой - ключи неправильно упорядочены - это отличное решение - и, конечно, оно просто остановится, если обнаружит конфликт, вместо слепой сериализации всего объекта.

Чтобы избежать импорта всей библиотеки, сделайте следующее:

import { isEqual } from "lodash-es";

Бонусный пример: вы также можете использовать это с RxJS с этим настраиваемым оператором

export const distinctUntilEqualChanged = <T>(): MonoTypeOperatorFunction<T> => 
                                                pipe(distinctUntilChanged(isEqual));

Я сделал функцию для сортировки объекта и с обратным вызовом.., которые на самом деле создают новый объект

function sortObj( obj , callback ) {

    var r = [] ;

    for ( var i in obj ){
        if ( obj.hasOwnProperty( i ) ) {
             r.push( { key: i , value : obj[i] } );
        }
    }

    return r.sort( callback ).reduce( function( obj , n ){
        obj[ n.key ] = n.value ;
        return obj;
    },{});
}

и назовите это с объектом.

var obj = {
    name : "anu",
    os : "windows",
    value : 'msio',
};

var result = sortObj( obj , function( a, b ){
    return a.key < b.key  ;    
});

JSON.stringify( result )

который печатает {"value":"msio","os":"windows","name":"anu"} и для сортировки по значению.

var result = sortObj( obj , function( a, b ){
    return a.value < b.value  ;    
});

JSON.stringify( result )

который печатает {"os":"windows","value":"msio","name":"anu"}

Не путайте с мониторингом объектов отладчиком Chrome. Он показывает отсортированные ключи в объекте, хотя на самом деле он не отсортирован. Вы должны отсортировать объект перед тем, как преобразовать его в строку.

Пытаться:

function obj(){
  this.name = '';
  this.os = '';
}

a = new obj();
a.name = 'X',
a.os = 'linux';
JSON.stringify(a);
b = new obj();
b.os = 'linux';
b.name = 'X',
JSON.stringify(b);

Если объекты в списке не имеют одинаковых свойств, создайте объединенный главный объект перед stringify:

let arr=[ <object1>, <object2>, ... ]
let o = {}
for ( let i = 0; i < arr.length; i++ ) {
  Object.assign( o, arr[i] );
}
JSON.stringify( arr, Object.keys( o ).sort() );

function FlatternInSort( obj ) {
    if( typeof obj === 'object' )
    {
        if( obj.constructor === Object )
        {       //here use underscore.js
            let PaireStr = _( obj ).chain().pairs().sortBy( p => p[0] ).map( p => p.map( FlatternInSort ).join( ':' )).value().join( ',' );
            return '{' + PaireStr + '}';
        }
        return '[' + obj.map( FlatternInSort ).join( ',' ) + ']';
    }
    return JSON.stringify( obj );
}

// пример как показано ниже. в каждом слое для объектов типа {}, сплющенных в сортировке ключей. для массивов, чисел или строк, сплющенных как / с JSON.stringify.

FlatternInSort ({c: 9, b: {y: 4, z: 2, e: 9}, F: 4, a: [{j: 8, h: 3}, {a: 3, b: 7}] })

"{" F ": 4," а ":[{" ч ": 3," J ":8},{" а ": 3," б ":7}]," б ": {" е": 9, "у": 4, "г":2},"С": 9}"

Расширение ответа AJP для обработки массивов:

function sort(myObj) {
    var sortedObj = {};
    Object.keys(myObj).sort().forEach(key => {
        sortedObj[key] = _.isPlainObject(myObj[key]) ? sort(myObj[key]) : _.isArray(myObj[key])? myObj[key].map(sort) : myObj[key]
    })
    return sortedObj;
}

Есть Array.sort метод, который может быть полезен для вас. Например:

yourBigArray.sort(function(a,b){
    //custom sorting mechanism
});
Другие вопросы по тегам