In this blogpost, we will see ways to loop through Object keys in reverse order in JavaScript. We’ll also explore Generators, ES6 Object property ordering nuances, and how an Array and Object differ in their ordering, avoiding mutation while iteration, and a common error with for…in loop while iterating Objects.
Lets dive in, and make you JavaScript Object Moon-Walking expert 🕺!
Refresher: Objects in JavaScript:
- JavaScript objects are collections of properties, where each property is a key-value pair. Usually, objects are iterated in the order of key insertion, but sometimes, you might need to traverse them backward.
const obj = { a: 'one', b: 'two', c: 'three' };
for (const key in obj) {
console.log(`${key}: ${obj[key]}`);
}
// Output:
// a: one
// b: two
// c: three
Ways to reverse iterate on JavaScript Set
Using Object.keys() with reverse():
- Extract keys using
Object.keys()
. - Reverse the array with
reverse()
. Note thatreverse()
mutates the array it acts upon. So here the array generated byObject.keys()
is mutated byreverse()
, but the object itself is untouched. - Iterate using
forEach()
.
const obj = { a: 'one', b: 'two', c: 'three' };
const reversedKeys = Object.keys(obj).reverse();
reversedKeys.forEach(key => {
console.log(`${key}: ${obj[key]}`);
});
// Output:
// c: three
// b: two
// a: one
Using spread operator (…) with Object.keys() and reverse()
- Extract keys using
Object.keys()
. - Reverse the array with
reverse()
. Note thatreverse()
mutates the array it acts upon. So here the array generated byObject.keys()
is mutated byreverse()
, but the object itself is untouched. - Iterate using
forEach()
.
const obj = { a: 'one', b: 'two', c: 'three' };
const keys = [...Object.keys(obj)];
const reversed = keys.reverse();
reversed.forEach(key => {
console.log(`${key}: ${obj[key]}`);
});
// Output:
// c: three
// b: two
// a: one
Using Object.values() with reverse():
- Extract keys using
Object.values()
. - Reverse the array with
reverse()
. Note thatreverse()
mutates the array it acts upon. So here the array generated byObject.values()
is mutated byreverse()
, but the object itself is untouched. - Iterate using
forEach()
.
const obj = { a: 'one', b: 'two', c: 'three' };
const reversedValues = Object.values(obj).reverse();
reversedValues.forEach(value => {
console.log(value);
});
// Output:
// three
// two
// one
Using Object.entries() with reverse():
- Extract keys using
Object.entries()
. - Reverse the array with
reverse()
. Note thatreverse()
mutates the array it acts upon. So here the array generated byObject.entries()
is mutated byreverse()
, but the object itself is untouched. - Iterate using
forEach()
.
const obj = { a: 'one', b: 'two', c: 'three' };
const reversedEntries = Object.entries(obj).reverse();
reversedEntries.forEach(([key, value]) => {
console.log(`${key}: ${value}`);
});
// Output:
// c: three
// b: two
// a: one
Using Generators
Generators provide a powerful way to create intricate iteration behavior, which can be useful while reverse iterating object keys. While simple loops can use break
and continue
, generators offer additional flexibility: like pausing or conditional continuation.
- State Preservation: Generators maintain their state, allowing for resumption of iteration at specific points.
- Custom Iteration Logic: Generators can yield values based on complex conditions, beyond simple breaks and continues.
function* reverseObjectIteratorWithPause(obj) {
const keys = Object.keys(obj).reverse(); // Reverse the keys
for (let key of keys) {
if (obj[key] === 'pause') {
yield 'Iteration paused';
} else {
yield `Key: ${key}, Value: ${obj[key]}`;
}
}
}
// Example object
const obj = { a: 1, b: 'pause', c: 3, d: 4 };
// Using the generator
const iterator = reverseObjectIteratorWithPause(obj);
console.log(iterator.next().value); // Key: d, Value: 4
console.log(iterator.next().value); // Key: c, Value: 3
console.log(iterator.next().value); // Iteration paused
console.log(iterator.next().value); // Key: b, Value: pause
- The generator
reverseObjectIteratorWithPause
iterates over the object’s keys in reverse order. - When it encounters a value equal to
'pause'
, it yields a message indicating that the iteration is paused. This showcases the ability of generators to handle conditional logic within iterations. - This level of control, especially with the ability to yield specific messages or pause iteration based on conditions, is not as straightforward with conventional loops.
Tips on reverse iterating Object keys in Javascript
ECMAScript 2015 (ES6): Object Property Enumeration Order
ES6 specifies property order in objects: integer keys in ascending order, followed by string and symbol keys in creation order. So they may not always be listed in the order in which you added them, and even your iteration order may not be as expected. Further, relying completely on even ES6 property order can be risky, especially when considering different JavaScript engines or older versions.
For now, lets illustrate this as per ES6 specs. Let’s consider an object with multiple numeric, string, and symbol keys.
const sym1 = Symbol('sym1');
const sym2 = Symbol('sym2');
const mixedObj = {
'30': 'numericKey1',
'10': 'numericKey2',
'b': 'stringKey1',
'a': 'stringKey2',
[sym1]: 'symbolKey1',
[sym2]: 'symbolKey2'
};
// Logs properties in the order: numeric keys in ascending order, string keys, symbol keys
console.log(Reflect.ownKeys(mixedObj));
// Output: ['10', '30', 'b', 'a', Symbol(sym1), Symbol(sym2)]
- Numeric keys (’10’, ’30’) are sorted numerically, followed by string keys (‘b’, ‘a’) in order of creation, and finally symbol keys (
sym1
,sym2
) in order of creation as well.
Iteration order differences in Array & Object
Arrays have a predictable iteration order, based on index positions. Object properties have a specific order as per ES6, but it’s different from arrays.
const arr = ['first', 'second', 'third'];
const obj = { '0': 'first', '2': 'third', '1': 'second' };
// Reverse Array Iteration
for (let i = arr.length - 1; i >= 0; i--) {
console.log('Array Order: ',arr[i]);
}
// Reverse Object Iteration
const keys = Object.keys(obj);
console.log("Keys: ", keys);
for (let i = keys.length - 1; i >= 0; i--) {
console.log('Object Order: ',obj[keys[i]]);
}
// Output: 👇
// Array Order: third
// Array Order: second
// Array Order: first
//
// Keys: [ '0', '1', '2' ]
// Object Order: third
// Object Order: second
// Object Order: first
- The order of elements in
arr
is index-based, whereasobj
follows ES6 property order rules. - The reverse iteration of
arr
andobj
demonstrates how array elements follow the index order, while object properties adhere to the specified ES6 order, which may differ from the array index order.
Inherited properties included in for…in loop
The for...in
loop in JavaScript iterates over the enumerable properties of an object, in the order defined as per ES6 specifications. However, it’s important to note that this loop iterates over all enumerable properties, including those inherited through the prototype chain.
But, by adding the Object.hasOwnProperty
check, you can ensure that only the object’s own properties are considered.
// Define a prototype with properties
const prototypeObj = {
inheritedProperty: 'inherited value'
};
// Create a new object that inherits from prototypeObj
const obj = Object.create(prototypeObj);
obj.ownProperty1 = 'value1';
obj.ownProperty2 = 'value2';
// Using for...in loop without hasOwnProperty check
for (let key in obj) {
console.log(`${key}: ${obj[key]}`);
}
// Output includes:
// ownProperty1: value1
// ownProperty2: value2
// inheritedProperty: inherited value
// - - Revised loop with hasOwnProperty - -
// Using for...in loop with hasOwnProperty check
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
console.log(`${key}: ${obj[key]}`);
}
}
// Output includes only the object's own properties:
// ownProperty1: value1
// ownProperty2: value2
- First, we create a prototype object and a new object that inherits from this prototype.
- We can see
inheritedProperty
is logged alongside the object’s own properties, demonstrating how thefor...in
loop includes inherited properties. - In this revised loop,
inheritedProperty
is not logged, asobj.hasOwnProperty(key)
filters out properties that are not directly onobj
but are inherited from its prototype.
So how do you reverse Object using for…in loop? You cant directly access index, so you’ll need to store the keys in an array, reverse this array, and then iterate over it.
const obj = { a: 'one', b: 'two', c: 'three' };
let keys = [];
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
keys.push(key);
}
}
keys.reverse(); // ['c', 'b', 'a']
keys.forEach(key => console.log(`${key}: ${obj[key]}`));
// Output:
// c: three
// b: two
// a: one
Avoid mutation while iteration
When iterating over an object’s properties, adding or removing elements during the iteration can lead to confusing behaviors.
In following example, at a particular iteration, we will remove an element already iterated, being iterated and going to be iterated, and then also add an element. Lets see what this mutation while iteration gives.
const obj = { a: 1, b: 2, c: 3 };
Object.keys(obj).forEach((key, index) => {
console.log(`Before mutation: ${key}, ${obj[key]}`);
if (key === 'b') {
delete obj['a']; // Deleting already iterated property
delete obj['b']; // Deleting an existing property
delete obj['c']; // Deleting upcoming iteration property
obj['d'] = 4; // Adding a new property
}
console.log(`After mutation: ${key}, ${obj[key]}`);
});
console.log('Final object:', obj);
// Output: 👇
// Before mutation: a, 1
// After mutation: a, 1
// Before mutation: b, 2
// After mutation: b, undefined
// Before mutation: c, undefined
// After mutation: c, undefined
// Final object: { d: 4 }
- Note that
Object.keys(obj)
has already produced an array with value[a,b,c]
, and we are iterating with forEach on this array of keys, but inside those iterations we are mutating elements from the Object itself. Also, once the array[a,b,c]
is produced byObject.keys()
, it is unlinked toobj
and will not reflect mutation of keys inobj
. - The first iteration (with key
a
) logs the current state of the object before any mutation. - In the second iteration (with key
b
), the object is mutated: current iteration key (b
), previous iteration key (a
) and next iteration key (c
) are deleted, and'd': 4
is added. - The console logs during iteration show how
a
is unaffected (since already iterated,b
got deleted after mutation, and thatc
is deleted for the next iteration. And thatd
is never iterated, since it wasn’t part of the array produced byObject.keys(obj)
. - The final state of the object, after the loop completes, reflects the deletions of
'a','b','c'
and the addition of'd'
. Only'd'
remains in the end.
Performance tips for Large Objects:
Object.keys()
and for loop is faster than Object.keys().reverse().forEach()
(where there is an additional reverse()
call).
const largeObj = {};
for (let i = 0; i < 1000_000; i++) {
largeObj[`key${i}`] = i;
}
// Technique 1: Using Object.keys() with reverse()
console.time('With reverse');
Object.keys(largeObj).reverse().forEach(key => {});
console.timeEnd('With reverse');
// Technique 2: Without using reverse()
console.time('Without reverse');
const keys = Object.keys(largeObj);
for (let i = keys.length - 1; i >= 0; i--) {
// Iteration logic here
}
console.timeEnd('Without reverse');
// Timing on my machine (for 1000_000 iterations)
// With reverse: 419.276ms
// Without reverse: 389.834ms
- Technique 1 involves the extra step of reversing the array of keys, which could impact performance, especially for large objects.
- Technique 2 avoids the
reverse()
method, potentially offering better performance for iterating over large objects.
🧪Practice Coding Problem: Backward Summation Safari 🌳🐎
In the spirit of Test Driven Development ( 😁), lets test our understanding by solving a problem.
Write a function which does a “Backward Summation Safari”, where it’ll traverse the properties of a JavaScript object in reverse order, adding up only numerical values it finds the trail.
Problem (JavaScript)/** * Goes on a safari through an object's properties in reverse, summing all numerical values. * @param {Object} obj - The object to explore. * @return {number} - The total sum of numerical property values encountered. */ function backwardSummationSafari(obj) { // > > > 👉 Write code here 👈 < < < } // Driver code const safariObject = { zebra: 3, lion: 7, elephant: 2, garbage: 'value' }; console.log(backwardSummationSafari(safariObject)); // Expected Output: The sum of the values (3 + 7 + 2 = 12) when iterated in reverse order // skipped pair (garbage: 'value').
Please attempt before seeing the Answer:
function backwardSummationSafari(obj) {
const keys = Object.keys(obj).reverse();
let totalSum = 0;
keys.forEach(key => {
if (typeof obj[key] === 'number') {
totalSum += obj[key];
}
});
return totalSum;
}
Explanation:
- The
backwardSummationSafari
function grabs all the property keys of the object and reverses their order. - Then initialises
totalSum
as 0, & iterates over the reversed keys. - And accumulates only numerical values in
totalSum
.
So you are an expert in Moonwalking Objects now. 🤓 Great! 🎉
But don’t stop, and keep coding! 🚀👨💻