开发者

How to sort a JavaScript array of objects by nested object property?

I have this function to sort a JavaScript array of objects based on a property:

// arr is the array of objects, prop is the property to sort by
var sort = function (prop, arr) {
    arr.sort(function (a, b) {
        if (a[prop] < b[prop]) {
            return -1;
        } el开发者_高级运维se if (a[prop] > b[prop]) {
            return 1;
        } else {
            return 0;
        }
    });
};

It works with arrays like this:

sort('property', [
    {property:'1'},
    {property:'3'},
    {property:'2'},
    {property:'4'},
]);

But I want to be able to sort also by nested properties, for example something like:

sort('nestedobj.property', [
    {nestedobj:{property:'1'}},
    {nestedobj:{property:'3'}},
    {nestedobj:{property:'2'}},
    {nestedobj:{property:'4'}}
]);

However this doesn't work because it is not possible to do something like object['nestedobj.property'], it should be object['nestedobj']['property'].

Do you know how could I solve this problem and make my function work with properties of nested objects?

Thanks in advance


You can split the prop on ., and iterate over the Array updating the a and b with the next nested property during each iteration.

Example: http://jsfiddle.net/x8KD6/1/

var sort = function (prop, arr) {
    prop = prop.split('.');
    var len = prop.length;

    arr.sort(function (a, b) {
        var i = 0;
        while( i < len ) { a = a[prop[i]]; b = b[prop[i]]; i++; }
        if (a < b) {
            return -1;
        } else if (a > b) {
            return 1;
        } else {
            return 0;
        }
    });
    return arr;
};


Use Array.prototype.sort() with a custom compare function to do the descending sort first:

champions.sort(function(a, b) { return b.level - a.level }).slice(...

Even nicer with ES6:

champions.sort((a, b) => b.level - a.level).slice(...


Instead of passing the property as a string, pass a function that can retrieve the property from the top level object.

var sort = function (propertyRetriever, arr) {
    arr.sort(function (a, b) {
        var valueA = propertyRetriever(a);
        var valueB = propertyRetriever(b);

        if (valueA < valueB) {
            return -1;
        } else if (valueA > valueB) {
            return 1;
        } else {
            return 0;
        }
    });
};

Invoke as,

var simplePropertyRetriever = function(obj) {
    return obj.property;
};

sort(simplePropertyRetriever, { .. });

Or using a nested object,

var nestedPropertyRetriever = function(obj) {
    return obj.nestedObj.property;
};

sort(nestedPropertyRetriever, { .. });


if you have array of objects like

const objs = [{
        first_nom: 'Lazslo',
        last_nom: 'Jamf',
        moreDetails: {
            age: 20
        }
    }, {
        first_nom: 'Pig',
        last_nom: 'Bodine',
        moreDetails: {
            age: 21
        }
    }, {
        first_nom: 'Pirate',
        last_nom: 'Prentice',
        moreDetails: {
            age: 22
        }
    }];

you can use simply

nestedSort = (prop1, prop2 = null, direction = 'asc') => (e1, e2) => {
        const a = prop2 ? e1[prop1][prop2] : e1[prop1],
            b = prop2 ? e2[prop1][prop2] : e2[prop1],
            sortOrder = direction === "asc" ? 1 : -1
        return (a < b) ? -sortOrder : (a > b) ? sortOrder : 0;
    }

and call it

for direct objects

objs.sort(nestedSort("last_nom"));
objs.sort(nestedSort("last_nom", null, "desc"));

for nested objects

objs.sort(nestedSort("moreDetails", "age"));
objs.sort(nestedSort("moreDetails", "age", "desc"));


You can use Agile.js for this kind of things.
Actually you pass an expression instead of callback, it's handle nested properties and javascript expression in a very nice-ish way.

Usage: _.orderBy(array, expression/callback, reverse[optional])

Example:

var orders = [
  { product: { price: 91.12, id: 1 }, date: new Date('01/01/2014') },
  { product: { price: 79.21, id: 2 }, date: new Date('01/01/2014') },
  { product: { price: 99.90, id: 3 }, date: new Date('01/01/2013') },
  { product: { price: 19.99, id: 4 }, date: new Date('01/01/1970') }
];

_.orderBy(orders, 'product.price');
// →  [orders[3], orders[1], orders[0], orders[2]]

_.orderBy(orders, '-product.price');
// → [orders[2], orders[0], orders[1], orders[3]]


Would this meet your needs?

// arr is the array of objects, prop is the property to sort by
var sort = function (nestedObj, prop, arr) {
    arr.sort(function (a, b) {
        if (a[nestedObj][prop] < b[nestedObj][prop]) {
            return -1;
        } else if (a[nestedObj][prop] > b[nestedObj][prop]) {
            return 1;
        } else {
            return 0;
        }
    });
};


Try this (used a recursive function to get nested value, you can pass the nested property as nestedobj.property): You can use this for any level of hierarchy

// arr is the array of objects, prop is the property to sort by
var getProperty = function(obj, propNested){
 if(!obj || !propNested){
  return null;
 }
 else if(propNested.length == 1) {
    var key = propNested[0];
    return obj[key];
 }
 else {
  var newObj = propNested.shift();
    return getProperty(obj[newObj], propNested);
 }
};
var sort = function (prop, arr) {
    arr.sort(function (a, b) {
                var aProp = getProperty(a, prop.split("."));
                var bProp = getProperty(a, prop.split("."));
        if (aProp < bProp) {
            return -1;
        } else if (aProp > bProp) {
            return 1;
        } else {
            return 0;
        }
    });
};


This is my modify code.

// arr is the array of objects, prop is the property to sort by
var s = function (prop, arr) {
    // add sub function for get value from obj (1/2)
    var _getVal = function(o, key){
        var v = o;
        var k = key.split(".");
        for(var i in k){
            v = v[k[i]];
        }
        return v;
    }
    return arr.sort(function (a, b) {
        // get value from obj a, b before sort (2/2)
        var aVal = _getVal(a, prop);
        var bVal = _getVal(b, prop);
        if (aVal < bVal) {
            return -1;
        } else if (aVal > bVal) {
            return 1;
        } else {
            return 0;
        }
    });
};


var objectsArr = [
  {nestedobj:{property:'1'}},
  {nestedobj:{property:'3'}},
  {nestedobj:{property:'2'}},
  {nestedobj:{property:'4'}}
];

function getFromPath(obj, path) {
  let r = obj;
  path.forEach(key => { r = r[key]})
  return r
}

function sortObjectsArr(objectsArray, ...path) {
  objectsArray.sort((a, b) => getFromPath(a, path) - getFromPath(b, path))
}

sortObjectsArr(objectsArr, 'nestedobj', 'property');

console.log(objectsArr);

Unfortunately, I didn't find any nice way to use the arguments in order to access the attributes of the nested object.
Want to mention that there can be some checks if the keys are available in the passed object, but this depends on who and how want to implement this.


Description

My solution is this one. I decide to flat the object first:

function flattenObject(value: any): any {
    let toReturn: any = {};

    for (const i in value) {
        if (!value.hasOwnProperty(i)) {
            continue;
        }

        if (typeof value[i] == 'object') {
            const flatObject = flattenObject(value[i]);
            for (const x in flatObject) {
                if (!flatObject.hasOwnProperty(x)) continue;

                toReturn[i + '.' + x] = flatObject[x];
            }
        } else {
            toReturn[i] = value[i];
        }
    }
    return toReturn;
}

And then I'll extract the value from the object:

function nestedFieldValue(
    nestedJoinedFieldByDot: string,
    obj: any,
): any {
    return flattenObject(obj)[nestedJoinedFieldByDot];
}

Ant at the end I just need to do this:

export function fieldSorter(fields: string[]) {
    return function (a: any, b: any) {
        return fields
            .map(function (fieldKey) {
                // README: Sort Ascending by default
                let dir = 1;

                if (fieldKey[0] === '-') {
                    // README: Sort Descending if `-` was passed at the beginning of the field name
                    dir = -1;
                    fieldKey = fieldKey.substring(1);
                }

                const aValue = nestedFlattenObjectFieldValue(
                    fieldKey,
                    a,
                );
                const bValue = nestedFlattenObjectFieldValue(
                    fieldKey,
                    b,
                );

                if (
                    typeof aValue === 'number' ||
                    typeof bValue === 'number'
                ) {
                    /**
                     * README: default value when the field does not exists to prevent unsorted array
                     * I assume that 0 should be the last element. In other word I sort arrays in a way
                     * that biggest numbers comes first and then smallest numbers
                     */
                    if (aValue ?? 0 > bValue ?? 0) {
                        return dir;
                    }
                    if (aValue ?? 0 < bValue ?? 0) {
                        return -dir;
                    }
                } else {
                    if (aValue ?? 0 > bValue ?? 0) {
                        return dir;
                    }
                    if (aValue ?? 0 < bValue ?? 0) {
                        return -dir;
                    }
                }

                return 0;
            })
            .reduce(function firstNonZeroValue(p, n) {
                return p ? p : n;
            }, 0);
    };
}

Finally we need to do this:

const unsorted = [ 
    { 
        city: { 
            priority: 1, 
            name: 'Tokyo', 
            airport: { name: 'Haneda Airport' } 
        }
    }
]

const result = unsorted.sort(
    fieldSorter(['city.priority', 'city.airport.name', 'city.name']),
);

I think this way is much much clear and cleaner. It is readable and more functional. I merge multiple answer from stackoverflow to reach this solution :sweat_smile:


This should work and can accept multiple parameters.

https://codepen.io/allenACR/pen/VwQKWZG

function sortArrayOfObjects(items, getter) {
  const copy = JSON.parse(JSON.stringify(items));

  const sortFn = fn => {
    copy.sort((a, b) => {
      a = fn(a)
      b = fn(b)
      return a === b ? 0 : a < b ? -1 : 1;
    });
  };

  getter.forEach(x => {
    const fn = typeof x === 'function' ? x : item => item[x];
    sortFn(fn);
  });

  return copy;
}

// example dataset
const data = [
  {id: 3, name: "Dave", details: {skill: "leader"} },
  {id: 1, name: "Razor", details: {skill: "music"} }, 
  {id: 2, name: "Syd", details: {skill: "animal husbandry"} }
]

// sort via single prop
const sort1 = sortArrayOfObjects(data, ["id"])
// returns [Razor, Syd, Dave]

// sort via nested
const sort2 = sortArrayOfObjects(data, [
  (item) => item.details.skill  
])
// returns [Syd, Dave, Razor]

console.log({sort1, sort2})


3 levels deep. path can look like this. 'level1' or 'level1.level2' or 'level1.level2.level3' I also did uppercase for the sort all my items are strings. Anwser is a modified version from - @Mas

public keysrt(arr: Object[], path: string, reverse: boolean): void {
    const nextOrder = reverse ? -1 : 1;
    const pathSplit = path.split('.');
    if (arr === null || arr === undefined ) {
        return;
    }
    if (arr.length <= 1) {
        return;
    }

    const nestedSort = (prop1, prop2 = null, prop3 = null, direction = 'asc') => (e1, e2) => {
        const a = prop3 ? e1[prop1][prop2][prop3] : prop2 ? e1[prop1][prop2] : e1[prop1],
            b = prop3 ? e2[prop1][prop2][prop3] : prop2 ? e2[prop1][prop2] : e2[prop1],
            sortOrder = direction === 'asc' ? 1 : -1;
        return (a.toString().toUpperCase() < b.toString().toUpperCase()) ?
            -sortOrder * nextOrder : (a.toString().toUpperCase() > b.toString().toUpperCase()) ?
                sortOrder * nextOrder : 0;
    };

    if (pathSplit.length === 3) {
        arr.sort(nestedSort(pathSplit[0], pathSplit[1], pathSplit[2]));
    }
    if (pathSplit.length === 2) {
        arr.sort(nestedSort(pathSplit[0], pathSplit[1]));
    }
    if (pathSplit.length === 1) {
        arr.sort(nestedSort(pathSplit[0], null));
    }
}


For those who a researching on how to sort nested properties. I created a small type-safe array sorting method with support for deeply nested properties and Typescript autocompletion.

https://github.com/jvandenaardweg/sort-by-property

https://www.npmjs.com/package/sort-by-property

Example:

blogPosts.sort(sortByProperty('author.name', 'asc'));
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜