Devpuff
Pricing
Log InStart Free
  1. Home
  2. Blog
  3. How to loop through an object in JavaScript
Learn

How to loop through an object in JavaScript

To loop through an object in JavaScript, for...in walks inherited properties too. And integer-like keys come out first, whatever order you wrote.

By Max Arthur
Co-Founder & Content Marketer·August 31, 2026·9 Min read
The same object iterated with Object.keys and with for-in, where for-in returns an extra inherited key

The default answer, and the one to reach for first:

for (const [key, value] of Object.entries(obj)) {
  console.log(key, value);
}

Object.entries gives you both halves at once, and the destructuring in the loop header names them. Four methods exist and they differ in ways that matter. Everything below was measured in Node v24.14.1.

The four ways

Object.keys(obj);      // ["b", "a"]
Object.values(obj);    // [2, 1]
Object.entries(obj);   // [["b", 2], ["a", 1]]

for (const key in obj) { }

The first three return real arrays, so every array method is available: map, filter, sort, reduce. That alone is usually enough reason to prefer them.

// filter an object
Object.fromEntries(
  Object.entries(obj).filter(([, v]) => v > 1)
);

Object.fromEntries is the inverse of Object.entries, which makes "transform an object" a round trip through an array. The empty slot in [, v] is destructuring that skips the key.

for...in walks the prototype chain

This is the difference that causes bugs:

const base = { inherited: "from prototype" };
const obj = Object.create(base);
obj.b = 2;
obj.a = 1;
Method Result
Object.keys(obj) ["b", "a"]
for (const k in obj) ["b", "a", "inherited"]

for...in returned a key that is not on the object. It walks up the prototype chain and reports every enumerable property it finds, including ones inherited from elsewhere.

For a plain object literal that never happens, because Object.prototype's own properties are not enumerable. It happens as soon as objects are created with Object.create, given a class, or handed to you by a library that puts helpers on a prototype.

The traditional guard:

for (const key in obj) {
  if (Object.hasOwn(obj, key)) {
    // ...
  }
}

Object.hasOwn is the modern form of Object.prototype.hasOwnProperty.call(obj, key), which is what older code writes to avoid the case where the object itself has a property named hasOwnProperty.

Measured, the guarded loop returns ["b", "a"], matching Object.keys.

The simpler conclusion: Object.keys already does what you meant. for...in needs a guard to be correct, and if you are adding the guard anyway, the static method is shorter and clearer.

Key order is not insertion order

Object.keys({ b: 1, 2: 1, a: 1, 1: 1 });
// ["1", "2", "b", "a"]

The integer-like keys came out first, sorted ascending, before the string keys. The string keys kept their insertion order among themselves.

That is specified behaviour, not an implementation quirk. Object property order is: integer-like keys in ascending numeric order, then string keys in insertion order, then symbols.

It surprises people who use numeric ids as keys and expect them to stay in the order they were added. If order matters, a Map preserves insertion order for every key type:

const m = new Map([["b", 1], [2, 1], ["a", 1]]);
[...m.keys()];   // ["b", 2, "a"]

That is one of the strongest arguments for Map over a plain object when the object is being used as a lookup table rather than as a record.

What these methods skip

Object.keys({ [Symbol("s")]: 1, a: 1 });   // ["a"]

Symbol keys are invisible to Object.keys, values, entries and for...in. That is deliberate: symbols exist partly so that a library can attach data to your object without appearing in iteration.

Non-enumerable properties are also skipped:

const o = {};
Object.defineProperty(o, "hidden", { value: 1, enumerable: false });
o.shown = 2;
Object.keys(o);   // ["shown"]

When you genuinely need everything, Reflect.ownKeys(obj) returns string and symbol keys, enumerable or not. It is rarely the right tool and worth knowing exists.

Looping in a specific order

Since key order is only partly under your control, sort explicitly when it matters:

Object.entries(obj)
  .sort(([a], [b]) => a.localeCompare(b))
  .forEach(([key, value]) => { });

Sorting by value is the same shape with the second element:

Object.entries(scores).sort(([, a], [, b]) => b - a);

localeCompare rather than < for the string case, for the reasons in sorting an array of objects.

Nested objects

None of these recurse. Object.entries on a nested object gives you the inner object as a value, not its contents.

function walk(obj, path = []) {
  for (const [key, value] of Object.entries(obj)) {
    if (value && typeof value === "object" && !Array.isArray(value)) {
      walk(value, [...path, key]);
    } else {
      console.log([...path, key].join("."), value);
    }
  }
}

The value && typeof value === "object" check is doing real work. typeof null is "object", so without the truthiness check a null value sends the function into a property access on null.

Objects, Maps and arrays

Worth a sentence each, because "loop through an object" sometimes means the object is the wrong shape.

A plain object is for records with known fields: a user, a config. Object.entries is the right iteration.

A Map is for a lookup table with arbitrary keys. It iterates directly with for...of, preserves insertion order for all key types, and has a size property that objects lack.

An array of objects is for an ordered list of records. If you find yourself iterating an object's values and ignoring the keys, the data probably wanted to be an array.

Object.entries(["a", "b"]);   // [["0", "a"], ["1", "b"]]

Note that array indices come back as strings through Object.entries, which is a good reminder that arrays are objects underneath.

Transforming an object

Because Object.entries gives you an array and Object.fromEntries turns one back, every array method becomes an object method:

const prices = { apple: 1.2, banana: 0.5, cherry: 3 };

// map the values
Object.fromEntries(
  Object.entries(prices).map(([k, v]) => [k, v * 2])
);
// { apple: 2.4, banana: 1, cherry: 6 }

// filter by value
Object.fromEntries(
  Object.entries(prices).filter(([, v]) => v < 2)
);
// { apple: 1.2, banana: 0.5 }

// rename the keys
Object.fromEntries(
  Object.entries(prices).map(([k, v]) => [k.toUpperCase(), v])
);

The round trip reads as one expression and covers most of what a lodash helper used to be needed for.

reduce handles the cases where the shape changes more:

// group an array of objects by a field
const byType = items.reduce((acc, item) => {
  (acc[item.type] ??= []).push(item);
  return acc;
}, {});

??= assigns only when the left side is nullish, which is what creates the array on first use. Current runtimes also have Object.groupBy(items, (i) => i.type) built in, which does exactly this and is worth using where available.

Iterating values only

When the keys do not matter:

for (const value of Object.values(obj)) { }

Worth pausing when you write that. An object whose keys are never used is usually an array wearing the wrong shape, and converting it upstream removes a whole class of ordering and iteration questions.

The legitimate case is a lookup table where you occasionally need to scan all the entries: a config keyed by name, a cache keyed by id. There the keys matter elsewhere even though this particular loop ignores them.

What it costs

Object.keys, values and entries each build a new array before you iterate it. For an object with a handful of properties that is irrelevant. Inside a hot loop over a large object it is real allocation.

for...in does not build an array, which is its one performance advantage, and it needs the Object.hasOwn guard to be correct. That trade is almost never worth taking deliberately; if iteration cost is genuinely the bottleneck, a Map is the better answer, since it iterates directly with no intermediate array:

for (const [key, value] of map) { }

No Object.entries call, no array allocated, and insertion order preserved for every key type.

Breaking out early

Object.entries with forEach cannot be stopped partway, because forEach ignores return and has no break. Three ways round it:

// for...of supports break
for (const [k, v] of Object.entries(obj)) {
  if (v === target) break;
}

// some() stops on the first true
Object.entries(obj).some(([k, v]) => v === target);

// find() when you want the pair back
Object.entries(obj).find(([, v]) => v === target);

for...of over Object.entries is the form worth defaulting to for exactly this reason: it reads the same as forEach and supports break, continue and return from the enclosing function.

some is the idiomatic early exit when the loop's purpose is a boolean, and it is worth using deliberately rather than as a trick, since a reader who does not know it will wonder why you are ignoring the return value.

Three common mistakes

Using for...in without a guard. It reports inherited enumerable properties. Object.keys does not.

Relying on insertion order with numeric keys. Integer-like keys sort first regardless of how you wrote them. Use a Map when order matters.

Assuming for...in on an array gives indices you can do arithmetic with. It gives string keys, and it also picks up any extra properties set on the array. Use for...of, or entries() if you need the index.

Looping in JSX

Rendering an object's entries in React is the same methods with one extra requirement:

{Object.entries(settings).map(([key, value]) => (
  <li key={key}>{key}: {String(value)}</li>
))}

Two details worth noting. The key prop wants the object key, which is naturally stable and unique, so this is one of the rare places where a good key requires no thought.

And String(value) matters because React throws on an object child. If any value in the object might itself be an object, rendering it directly produces the error covered in the "objects are not valid as a React child" error.

When the object came from JSON

Data parsed from an API is plain objects all the way down, which makes it the friendliest case: no prototype chain to worry about, no symbols, no non-enumerable properties. Object.entries sees everything there is.

Two habits help:

Check the shape before iterating. Object.entries(null) throws, and an endpoint that returns null instead of {} on an empty result is common:

Object.entries(data ?? {});

Do not assume the keys. Iterating is often the right response to data whose keys you do not control, such as a map of feature flags or per-currency prices. If you do know the keys, destructuring is clearer than a loop:

const { name, email } = user;

A loop over an object whose keys you already know is usually a loop that did not need to exist.

Quick reference

Object.keys(obj)                       // own enumerable string keys
Object.values(obj)                     // their values
Object.entries(obj)                    // [key, value] pairs
Object.fromEntries(pairs)              // back to an object
for (const k in obj)                   // includes INHERITED keys
Object.hasOwn(obj, k)                  // the guard for for...in
Reflect.ownKeys(obj)                   // everything, including symbols

Want to see the prototype chain leak into a loop? Start with the JavaScript track, or read how to check if an object is empty for the related question these methods also answer.

Keep reading

More from the blog

Two arrays combined by spread into a new array, beside push spreading into an existing one and overflowing the stack
September 1, 2026·9 min readLearn

How to merge two arrays in JavaScript

To merge arrays in JavaScript, spread and concat both work. push(...arr) throws a RangeError at 200,000 items, which is why the choice matters.

Read more
A spread copy sharing its nested object with the original beside a deep clone with its own separate nested object
August 29, 2026·8 min readLearn

Shallow vs deep copy in JavaScript: how to clone properly

To copy an object in JavaScript, spread copies the top level only, so nested objects stay shared. Here is what each method keeps and destroys.

Read more
{ }
✦

Ready to write some code?

Put this into practice - start your first free lesson. No setup, no credit card.

Start learning free
Devpuff

Learn to code by doing. One tiny, playful lesson at a time.

Learn
ProgramsCoursesPricing
Company
AboutBlogResourcesAffiliates
Support
Help CenterContactStatus
Programs
Frontend DevelopmentFull-Stack DevelopmentPython Developer
Courses
Advanced ReactAsync JavaScriptAsync PythonCSS BasicsCSS LayoutDSA Basics
Learn to Code
Learn JavaScriptLearn PythonLearn SQLLearn HTMLLearn CSSLearn ReactBrowse All Topics
Platform Comparisons
Devpuff vs CodecademyDevpuff vs MimoDevpuff vs Sololearn
© 2026 Devpuff. All rights reserved.Privacy PolicyTerms and ConditionsCookies PolicyRefund Policy
Devpuff vs freeCodeCamp
Read All Comparisons