Better for loops with values and keys

Currently for-loops are very bad and it's always so irritating to find the best way to loop over something, especially with TypeScript. There needs to be one standard way to loop over things that isn't annoying and allows getting values and keys without having to do manually, which often makes TypeScript scream at you.

For example if I want the keys and values of an object, I have to do:

for (const key in object) {
    const value = object[key] // It's impossible for TypeScript to know this is a key of the object and errors
}

Some people use Object.entries() instead but this is a bad solution to a fundamental problem and it involves looping over it twice.

So I have two suggestions, both of them involve a new well-known symbol, named something like Symbol.iteratorKey or Symbol.iteratorValueKey. This works just like Symbol.iterator, but it returns both the key and the value (how it will do this can be discussed later, for now I'll assume it will be an object with key and value properties).

Suggestion 1:
a new loop, for example for...ofin (a better name can also be discussed later), used like this:

for (const key, const value ofin object) {...}

Pros:

  • no conflicts with other loops
    Cons:
  • better name needed
  • too many loops already

Suggestion 2:
Just add this functionality to the existing for...of loop, since it just an improvement of that.

for (const key, const value of object) {...}

Pros:

  • easy to understand
  • no new loops
    Cons:
  • conflicts with map, since map already returns keys and values, though i don't think this matters, because it won't break anything and everyone already uses both the keys and the values, noone ever needs just the values. i don't think it will be confusing either, because someone who doesn't know about this probably won't be using map and even in javascript, the IDE can pick up that something is a map and give relevant type hints.

Are you suggesting installing this new will-known symbol on every object (e.g. on Object.prototype?), and that's what's powering this new loop?

If we're worried about the performance of Object.entries(), I personally would prefer adding a new function like Object.iterEntries() that returns an iterator instead of a list, as opposed to adding new syntax to and protocol specifically designed to solve this one problem.

I would be very surprised if the iterator protocol was faster than array methods iterating over Object.entries.

"this one problem". This is an extremely common problem and I also don't like feeling like I'm doing something wrong whenever I want to do this one thing.

What does this have to do with the iterator protocol or array methods? Object.entries needs to loop over the object so that you can loop over it again. Looping over something once is definitely faster than looping over something twice.

There's absolutely nothing wrong with using Object.keys/values/entries and then using array methods to iterate over that. If you continue to feel like there is, then I don't think that's something this discourse can help with :-)

O(n) and O(2n) are the same in terms of complexity, and are thus effectively the same speed.

The issue with the iterator protocol is that the spec mandates a bunch of observable function lookups and temporary object creations, which are difficult to optimize away.

It is a common problem and no you're not doing anything wrong. But it just feels weird to me to have special syntax that's specifically tailored to iterating over objects use-case - the fact that this syntax isn't very useful for other data types means it's not a very general-purpose syntax.

If you had to type "fjrkbfbidbebfbkfbf" to loop over an object, would you be okay with it? No, obviously not, even though it technically works. Why should I have to do this weird ass thing to do something basic? You don't have to use this if you have to, but if it gets introduced, I bet you'll use it too. We shouldn't avoid progress just because someone is narrow minded and doesn't like change

I realize this is a little nitpicky, but your statement that "it's impossible for TypeScript to know this is a key of the object and errors" isn't quite true. You just need to type the object you're iterating over, which makes sense given the fact that Typescript is already being used.

const object: Record<string, object> = {}
for (const key in object) {
  const value = object[key] ;
}

What do you think this has to do with the iterator protocol? And how do think Object.entries works? Whatever you're saying, it makes no sense to me.

Every broken clock is right twice a day. Every key is inferred as string and the keys of that object are all string. If you have a function that only returns 3, don't say it's a function that adds two numbers and use 1 and 2 as inputs.

I was responding to the suggestion of a new API method that returns an iterator over an object.

You lost me. You mind writing a function out to further illustrate your point?

When it comes to languages with optimising jit compilers what can make a bigger difference is which paths are the most optimised. Looping over something twice can be faster than looping over it once if each iteration is doing half the amount of work.

Even when looping over something once there can be clear differences in time, for example:

for (const k of preAllocatedKeysOfObj) {
	total += obj[k];
}

vs

for (let k in obj) {
   total += obj[k];	
}

Both loops iterate once, but the latter is being reported as being 4x faster (on my machine, in Safari).

5M ops/s ± 0.32%
77.4 % slower

vs

22M ops/s ± 1.76%
Fastest

While Object.entries is specified as returning a pre-allocated Array, this isn't what has to happen at runtime. A JS engine could optimise for (const [k, v] of Object.entires(obj)) { ... } if they detected that the keys of obj were not modified within the loop allowing it to be looped over once because the code would not be able to observe that the runtime did something different from the specification. In other words, the specification only defines what must be observed.

1 Like

Worth noting for ... in conditioned by hasOwnProperty is about as fast as Object.keys iteration even in microbenchmarks, and Object.entries suffers due to all the extra array allocations.

I suspect any way to bypass the array to have a significant performance impact in virtual DOM libraries and frameworks. I even got a significant (>1%) overall perf boost changing multiple instances of Object.keys(o).length === 0 to using a for ... in loop that breaks on first property where hasOwnProperty returns false. Don't have hard numbers, and this was years ago, so it'd take me a while to provide them. (I still recall years ago shaving ~5% off an internal framework benchmark once by simply moving a typeof value === "function" test after a couple == null tests, without any intermediate property accesses.)

Conditioned for ... in also has the side effect for normal objects of generating less garbage, which is helpful for reducing GC pressure (and related spikes) during rendering.

So, an iterable version of Object.entries would be valuable, especially if engines can detect and optimize it similarly to how they already do for arrays (would of course require type IC checks). But even if they couldn't, the two-element arrays could be immediately destructured and the whole result would just be cheap nursery allocations, improving performance anyways for more than maybe 1-2 properties. Iterable versions of Object.keys and Object.values I suspect would end up a little faster in practice as well.

1 Like

With language design, it's all a balance. We can't just add new syntax for every common thing we do in a language, it makes the language overwhelming to learn and use. For example, it's extremely common to map over the elements of an array, does that mean we should provide dedicated syntax to help with this? If we added such syntax, would you use it? Sure, probably, I would. Would it be nicer to use than the old-school map function? Sure. Does that mean we should add such syntax? No. In fact, there's growing concern of JavaScript becoming bloated because it already has so much syntax.

There's many things to weigh in when trying to decide if syntax should be added - how often it would get used is just one item on that scale. For me and my own subjective balance, the benefits it provides don't outweigh the little bit of bloat it adds to the language (along with some of the other minor cons). You don't have to agree with my opinion, and that's fine.

3 Likes

Nope, because this is modifying an existing syntax, not adding one, so your analogy doesn't work.

And also this is barely a new syntax. It's a comma and then another of a thing that is already there, not a brand new syntax, so your analogy is disingenuous. Does it really make it harder to learn JS though? I swear if addition didn't exist in JavaScript and there was a proposal to add it, someone would try to argue it's too hard to learn. Why do people make this silly argument because I hear it all the time.

It's a brand new syntax, objectively, because what you want won't parse right now. Modifying an existing syntax IS NEW SYNTAX, by definition.

It's not a silly argument; it's always a consideration when adding features.

1 Like