ES5까지는 for (var i = 0; i < arr.length; i++) 방식으로 순회했다. ES6부터는 for...of가 생겼는데, 이게 그냥 편의 문법이 아니라 이터러블·이터레이터 프로토콜이라는 명확한 규약 위에 만들어진 것이다.
Array, Set, Map은 모두 [Symbol.iterator]() 메서드를 가진다. for...of는 이걸 호출해서 이터레이터를 꺼내고, next를 반복 호출하며 순회한다.
const arr = [1, 2, 3];
let iter = arr[Symbol.iterator]();
iter.next(); // {value: 1, done: false}
iter.next(); // {value: 2, done: false}
iter.next(); // {value: 3, done: false}
iter.next(); // {value: undefined, done: true}
arr[Symbol.iterator] = null로 바꾸면 for...of가 TypeError를 던진다. for...of가 Symbol.iterator를 통해 동작한다는 증거다.
Set은 인덱스 접근이 안 되지만 for...of는 된다. Map도 .keys, .values, .entries가 모두 이터러블을 반환한다.
const map = new Map([['a', 1], ['b', 2], ['c', 3]]);
for (const a of map.keys()) log(a); // a b c
for (const a of map.values()) log(a); // 1 2 3
for (const a of map.entries()) log(a); // ['a', 1] ...
이터러블 프로토콜을 직접 구현할 수도 있다.
[Symbol.iterator]()를 가진 객체{value, done} 객체를 반환하는 next를 가진 객체const iterable = {
[Symbol.iterator]() {
let i = 3;
return {
next() {
return i == 0 ? {done: true} : {value: i--, done: false};
},
[Symbol.iterator]() { return this; }
};
}
};
for (const a of iterable) log(a); // 3 2 1
이터레이터가 자기 자신을 [Symbol.iterator]()로 반환하면, 이터레이터 자체도 for...of로 순회할 수 있다. 이를 well-formed 이터러블이라고 한다.
const a = [1, 2];
const set = new Set([3, 4]);
const map = new Map([['k', 5]]);
log([...a, ...set, ...map.values()]); // [1, 2, 3, 4, 5]
for...of, 전개 연산자, 구조 분해 모두 같은 규약 위에 있다.