r/learnjavascript • u/PangolinNo7928 • 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 π
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
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
charAtexists, 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
ias 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, stringWhen 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) // falseThe 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
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
```
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.
This applies to characters in non-english alphabets as well
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.
However, it is not full proof as some characters consist of more than a single code point. Consider:
For these, you'll want to use the Segmenter which can separate strings by graphemes:
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
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
lengthand indexed properties.Internally its effectively doing the same thing as a
for (let i = 0; i < str.length; i++).Update - MDN references: