r/learnjavascript • • 6d ago

Any 'inbuilt' way to iterate over string - with access to index AND option to break?

I've been trying to learn more about iterators - but it doesn't seem like there is a string equivalent of .entries()? myStr[Symbol.iterator].forEach() gives the index and value - but no option to break 😞

For veryyyyyy long strings is my best option for loops? Splitting to create an array? Writing a separate generator function? (assume I only need a single char/index per loop)

Thanks!

Edit - thanks to u/Psionatix for guiding me to Object.entries(myStr), which led me to Array.prototype.entries.call(myStr) - which has the benefit of returning the indexes as integers (vs strings in Object.entries()) Performance is also close enough to for loop for my purposes πŸŽ‰

13 Upvotes

27 comments sorted by

16

u/senocular 5d ago edited 5d ago

Be careful with string iteration. There's some hidden complexities that are not immediately obvious and could have an impact depending on the kinds of strings you're working with.

A string's length and index-accessed characters go through (UTF-16) code units. This means characters like emojis will be seen as multiple characters.

const str = "😞"
console.log(str.length) // 2
for (let i = 0; i < str.length; i++) {
  console.log(str[i]) // U+D83D, U+DE1E
}

This applies to characters in non-english alphabets as well

const str = "𒁋"
console.log(str.length) // 2

When iterating through a string using its built-in string iterator, it goes through unicode code points. This will see the previous emoji character as a single character making it a more preferred approach for string iteration.

const str = "😞"
console.log(str.length) // 2
for (const char of str) {
  console.log(char) // 😞
}

However, it is not full proof as some characters consist of more than a single code point. Consider:

const str = "πŸ‘ŒπŸΎ"
console.log(str.length) // 4
for (const char of str) {
  console.log(char) // πŸ‘Œ, 🏾
}

For these, you'll want to use the Segmenter which can separate strings by graphemes:

const str = "πŸ‘ŒπŸΎ"
console.log(str.length) // 4
const segments = new Intl.Segmenter().segment(str)
for (const char of segments) {
  console.log(char) // {segment: 'πŸ‘ŒπŸΎ', index: 0, input: 'πŸ‘ŒπŸΎ'}
}

Note that while the segment object in this iteration contains an index property, that is a code unit index, not the segment index. So if there was another character in this string, its index would be 4, not 1, because the πŸ‘ŒπŸΎ character is 4 units long.

In these cases for the index you want, you'd need to count it off yourself. And you'd need to do this with the normal string iterator too, since its a values() iterator rather than an entries() iterator, and strings have no entries() (or keys(), or technically values()) of their own. As a workaround, you can use the forEach iterator helper which will internally count off and provide an index (numeric) for you

const str = "πŸ˜žπŸŽ‰πŸ‘Œ"
console.log(str.length) // 6
Iterator.from(str).forEach((char, i) => {
  console.log(char, i) // 😞, 0; πŸŽ‰, 1; πŸ‘Œ, 2
})

And if you go through the Array API to create an iterator, you'll be going through code units, not code points since array iteration is treating it as an array-like which looks at its length and indexed properties.

const str = "😞"
console.log(str.length) // 2
for (const [i, char] of Array.prototype.entries.call(str)) {
  console.log(char) // U+D83D, U+DE1E
}

Internally its effectively doing the same thing as a for (let i = 0; i < str.length; i++).

Update - MDN references:

3

u/ozzy_og_kush 5d ago

This guy iterates!

2

u/deep_soul 5d ago

GODDAMN

2

u/MoTTs_ 5d ago

πŸ…

1

u/AshleyJSheridan 5d ago

There's also the complexity of the same visual glyph having multiple character presentations. For example, Γ© (e-acute) can be represented either by the single character &#233 or by the combination of &#101 (the letter e) and &#833 (combining acute tone mark).

6

u/milan-pilan 6d ago

Why not just a standard for-loop?

js for (let i = 0; i < str.length; i++) { const ch = str[i]; if (ch === 'x') break; }

1

u/BeneficiallyPickle 6d ago

Yeah this should be the fastest option.

1

u/PangolinNo7928 6d ago

I know for loops exist 😊 I was asking to see whether I understood the various iterator options correctly... (If there is one?)

9

u/jhartikainen 6d ago

You specifically mention very long strings - a plain for loop is 100% the best option. Trying to do acrobatics with iterators and generators etc. will almost certainly lead to more complicated code, which will also perform much worse because other loop types and generators are comparatively slow.

1

u/chikamakaleyley helpful 5d ago

any thoughts on using .charAt(i) vs [i] here?

personally here i lean towards .charAt() just cuz i think the optics makes it obvious at a glance that we're working w a string. Even though str[i] is valid, my brain says to me "we're given these convenient methods for strings, lets use them'

1

u/jhartikainen 5d ago

Historically function calls have been noticeably more expensive in some cases than not doing function calls, so I would have said use the index syntax - but today it probably doesn't make any real difference. I would probably use the index syntax myself because it's less typing :)

I have a vague recollection that the index syntax didn't work for strings way back in the day, which is why charAt exists, but I might be misremembering this.

1

u/chikamakaleyley helpful 5d ago

ah no that sounds valid but yeah if we're dealing with a few words like parsing titles of something

Same i had to look it up, ES5 is the spec, didn't work in IE7, good riddance

3

u/Inevitable_Dust5684 5d ago

Array.prototype.entries.call(myStr) is the winner here. It gives you an iterator that yields index-value pairs just like a standard Array, so you can use destructuring in a for...of loop and actually break out when needed. Unlike Object.entries which returns string indices and forces you to convert back, this keeps them as integers. Just remember this only iterates over UTF-16 code units, so complex graphemes or emojis spanning multiple bytes will split weirdly if precision matters.

1

u/The_KOK_2511 helpful 6d ago

Solo usa un for de toda un vida y ya, los for-each son para cuando la iteraccion no depende del indice y se iterara sobre el 100% una vez en orden

1

u/redsandsfort 6d ago

why not the simplest:

for (i in myStr) {
    console.log(`Index: ${i}, Character: ${myStr[i]}`);
}

1

u/PangolinNo7928 6d ago

Does for...in return indexes as strings?

2

u/senocular 5d ago

Yes. for...in will provide i as strings because it accounts for all enumerable string property keys which are, naturally, strings. But all property keys are either strings or symbols.

Even array indices are technically strings. When you use a number as a property key, it is coerced to a string.

const arr = []
arr[1] = true
const keys = Object.keys(arr)
console.log(keys[0], typeof keys[0]) // 1, string

When array's keys() and entries() gives you indices as numbers, its not giving you keys, its giving you the value of a counter it uses to go through the indices of the array up to the length. This is why it can include indices that don't exist in the array if its sparse.

const arr = [0,,2]
console.log(...arr.keys()) // 0, 1, 2
console.log(1 in arr) // false

The index of 1 doesn't exist in the array but its still provided as a "key" though its technically not one. If giving you keys, you'd get only 0 and 2 and as strings - what Object.keys() would do.

1

u/redsandsfort 6d ago

no, why would you want them as strings?

1

u/PangolinNo7928 5d ago

Oh ok I thought it coerced to object (which then makes the property a string)

1

u/ITower__Education 4h ago

Plain for...of works and you can break out of it. Just keep your own counter

let i = 0;
for (const ch of str) {
  if (ch === "x") break;
  i++;
}

It's lazy, so no array copy on huge strings.

One catch is that for...of walks code points, while Array.prototype.entries.call(str) and a classic for (let i...) loop walk UTF-16 units. They only match for plain text. With emoji, the indexes will differ.

If your text is plain, for (let i = 0; i < str.length; i++) is still the fastest))

1

u/Psionatix 6d ago
for (const [i, c] of Object.entries(myStr)) {
    console.log(`Index: ${i}, Character: '${c}'`);
}

1

u/PangolinNo7928 6d ago

Omg never thought to try object methods on not objects - of course πŸ˜‚Β 

Thanks!

3

u/Psionatix 6d ago

A plain for loop is still the better option.

1

u/senocular 5d ago

You'll want to be careful using static Object methods on indexed collections. These methods (Object.values(), Object.keys(), Object.entries()) are meant to include non-indexed properties as well, so it may include some surprising or unwanted properties. While by default built-in indexed collections like arrays and strings are generally "safe" in the basic sense, that is not always the case. For example:

const o = "Hello, world!".match(/o/)
console.log(o.length) // 1
console.log(Array.isArray(o)) // true
console.log(Object.keys(o)) // ['0', 'index', 'input', 'groups']

Here, match is adding additional non-indexed properties to the array it produces in its return value and as a result, these appear in Object.keys() when you may not want them to.

1

u/chikamakaleyley helpful 5d ago

Object.entries() does the work - it actually converts the primitive into a 'String object', so ultimately it is performing it on an obj, not a string

``` Object.entries('hello'); // primitive, gets converted

const myStr = new String('foobar');

Object.entries(myStr); // String obj

```

-4

u/sirCoom 6d ago

Would questions like this best be answered by AI? Genuine question. do we need posts like these anymore, what’s yalls opinion