work
This commit is contained in:
parent
1bffa43db4
commit
057783d216
373 changed files with 203 additions and 190 deletions
202
1-js/3-object-basics/8-iterable/article.md
Normal file
202
1-js/3-object-basics/8-iterable/article.md
Normal file
|
@ -0,0 +1,202 @@
|
|||
|
||||
# Iterables
|
||||
|
||||
*Iterable* objects is a general concept that allows to make any object useable in a `for..of` loop.
|
||||
|
||||
Many built-ins are partial cases of this concept. For instance, arrays are iterable. But not only arrays. Strings are iterable too.
|
||||
|
||||
Iterables come from the very core of Javascript and are widely used both in built-in methods and those provided by the environment.
|
||||
|
||||
[cut]
|
||||
|
||||
## Symbol.iterator
|
||||
|
||||
We can easily grasp the concept of iterables by making one of our own.
|
||||
|
||||
For instance, we have an object, that is not an array, but looks a suitable for `for..of`.
|
||||
|
||||
Like a `range` object that represents an interval of numbers:
|
||||
|
||||
```js
|
||||
let range = {
|
||||
from: 1,
|
||||
to: 5
|
||||
};
|
||||
|
||||
// We want for..of to work:
|
||||
// for(let num of range) ... num=1,2,3,4,5
|
||||
```
|
||||
|
||||
To make the `range` iterable (and thus let `for..of` work) we need to add a method to the object named `Symbol.iterator` (a special built-in symbol just for that).
|
||||
|
||||
- When `for..of` starts, it calls that method (or errors if none found).
|
||||
- The method must return an *iterator* -- an object with the method `next`.
|
||||
- When `for..of` wants the next value, it calls `next()` on that object.
|
||||
- The result of `next()` must have the form `{done: Boolean, value: any}`: `done:true` means that the iteration is finished, otherwise `value` must be the new value.
|
||||
|
||||
Here's the full impelementation for `range`:
|
||||
|
||||
```js run
|
||||
let range = {
|
||||
from: 1,
|
||||
to: 5
|
||||
}
|
||||
|
||||
// 1. call to for..of initially calls this
|
||||
range[Symbol.iterator] = function() {
|
||||
|
||||
// 2. ...it returns the iterator:
|
||||
return {
|
||||
current: this.from, // remember "from" and "to" of the range
|
||||
last: this.to, // in object properties
|
||||
|
||||
// 3. next() is called on each iteration of the loop
|
||||
next() {
|
||||
if (this.current <= this.last) {
|
||||
// 4. iterator returns the value as an object {done:.., value :...}
|
||||
return { done: false, value: this.current++ };
|
||||
} else {
|
||||
return { done: true };
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
for (let num of range) {
|
||||
alert(num); // 1, then 2, 3, 4, 5
|
||||
}
|
||||
```
|
||||
|
||||
There is an important separation of concerns in this code.
|
||||
|
||||
- The `range` itself does not have the `next()` method.
|
||||
- Instead, another object, a so-called "iterator" is created by the call to `range[Symbol.iterator]()`.
|
||||
- It keeps the iteration state in its `current` property. That's good, because the original object is not modified by iterations. Also multiple `for..of` loops over the same object can run simultaneously, because they create separate iterators.
|
||||
|
||||
The fact that the object itself does not do the iteration also adds flexibility, because `range[Symbol.iterator]` can create iterators the smart way, depending on other object properties or external conditions. It's a full-fledged function that may be more complex than just a `return {...}`.
|
||||
|
||||
Please note that the internal mechanics is not seen from outside. Here `for..of` calls `range[Symbol.iterator]()` and then the `next()` until `done: false`, but the external code doesn't see that. It only gets values.
|
||||
|
||||
```smart header="Infinite iterators"
|
||||
Infinite iterators are also doable. For instance, the `range` becomes infinite for `range.to = Infinity`. Or we can make an iterable object that generates an infinite sequence of pseudorandom numbers. Also can be useful.
|
||||
|
||||
There are no limitations on `next`, it can return more and more values, that's normal.
|
||||
|
||||
Of course, the `for..of` loop over such an iterable would be endless, we'll need to stop if, for instance, using `break`.
|
||||
```
|
||||
|
||||
````smart header="`Symbol.iterator` in a literal"
|
||||
We could also write `Symbol.iterator` directly in the object literal, via computed properties syntax:
|
||||
|
||||
```js
|
||||
let range = {
|
||||
from: 1,
|
||||
to: 5,
|
||||
[Symbol.iterator]() {
|
||||
return {...};
|
||||
}
|
||||
};
|
||||
```
|
||||
````
|
||||
|
||||
## Built-in iterables
|
||||
|
||||
Arrays and strings are most widely used built-in iterables.
|
||||
|
||||
For a string, `for..of` loops over its characters:
|
||||
|
||||
```js run
|
||||
for(let char of "test") {
|
||||
alert( char ); t, then e, then s, then t
|
||||
}
|
||||
```
|
||||
|
||||
Iterators can also be created explicitly, without `for..of`, with a direct call of `Symbol.iterator`. For built-in objects too.
|
||||
|
||||
For instance, this code gets a string iterator and calls it "manually":
|
||||
|
||||
```js run
|
||||
let str = "Hello";
|
||||
|
||||
// does the same as
|
||||
// for (let char of str) alert(char);
|
||||
|
||||
let iterator = str[Symbol.iterator]();
|
||||
|
||||
while(true) {
|
||||
let result = iterator.next();
|
||||
if (result.done) break;
|
||||
alert(result.value); // outputs characters one by one
|
||||
}
|
||||
```
|
||||
|
||||
The same works for an array.
|
||||
|
||||
## Iterables VS array-likes
|
||||
|
||||
There are two official terms that are similar, but actually very different. Please take care to avoid the confusion.
|
||||
|
||||
- Iterables are objects that implement the `Symbol.iterator` method, as described above.
|
||||
- Array-likes are objects that have indexes and `length`, so they look like arrays.
|
||||
|
||||
Sometimes they can both be applied. For instance, strings are both iterable and array-like.
|
||||
|
||||
But an iterable may be not array-like and vise versa.
|
||||
|
||||
For example, the `range` in the example above is iterable, but not array-like, because it does not have indexed properties and `length`.
|
||||
|
||||
And here's the object that is array-like, but not iterable:
|
||||
|
||||
```js run
|
||||
let arrayLike = { // has indexes and length => array-like
|
||||
0: "Hello",
|
||||
1: "World",
|
||||
length: 2
|
||||
};
|
||||
|
||||
*!*
|
||||
// Error (not Symbol.iterator)
|
||||
for(let item of arrayLike) {}
|
||||
*/!*
|
||||
```
|
||||
|
||||
But what they share in common -- both iterables and array-likes are usually *not arrays*, they don't have `push`, `pop` etc. That's rather inconvenient if we received such object and want to work with it as with an array.
|
||||
|
||||
There's a universal method [Array.from](mdn:js/Array/from) that brings them together. It takes an iterable *or* an array-like value and makes a "real" `Array` from it. Then we can call array methods on it.
|
||||
|
||||
For instance:
|
||||
|
||||
```js run
|
||||
let arrayLike = {
|
||||
0: "Hello",
|
||||
1: "World",
|
||||
length: 2
|
||||
};
|
||||
|
||||
let arr = Array.from(arrayLike);
|
||||
alert(arr.pop()); // World (method works)
|
||||
```
|
||||
|
||||
```js
|
||||
// assuming that range is taken from the example above
|
||||
let arr = Array.from(range);
|
||||
alert(arr); // 1,2,3,4,5 (toString of Array gives a list of items)
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
||||
Objects that can be used in `for..of` are called *iterable*.
|
||||
|
||||
- Technically, iterables must implement the method named `Symbol.iterator`.
|
||||
- The result of `obj[Symbol.iterator]` is called an *iterator*. It handles the further iteration process.
|
||||
- An iterator must have the method named `next()` that returns an object `{done: Boolean, value: any}`, here `done:true` denotes the iteration end, otherwise the `value` is the next value.
|
||||
- The `Symbol.iterator` method is called automatically by `for..of`, but we also can do it directly.
|
||||
- Built-in iterables like strings or arrays, also implement `Symbol.iterator`.
|
||||
|
||||
The modern specification mostly uses iterables instead of arrays where an ordered collection is required, because they are more abstract. We'll see more examples of it very soon.
|
||||
|
||||
Objects that have indexed properties and `length` are called *array-like*. Such objects may also have other properties and methods, but lack built-in methods of arrays.
|
||||
|
||||
`Array.from(obj)` makes a real `Array` of an iterable or array-like `obj`, and then we can use array methods on it.
|
||||
|
||||
|
Loading…
Add table
Add a link
Reference in a new issue