for...of 이건 되고 이건 왜 안됨?
iterable, iterator에 대해 직접적으로 공부하면 감이 잘 안 옵니다. 우선, javascript의 여러 for문을 살펴보도록합시다.
고전적 for문과 es6 이상부터 가능해진 for...of 와 for... in은 비슷해보이지만 차이가 존재합니다.
간단하게 정리하자면,
- 고전적 for문 : length 측정 후 하나씩 looping
- for...in : 객체를 순회합니다.
- for...of : iterable을 looping. (한국어판에서는 '순회 가능한 객체'라고 번역해놨는데 오히려 더 헷갈림. 이터러블이라고 부르자)
const arr = [1, 2, 3];
// 고전적인 for문. length에 의존하여 index를 올리는 방식
for (let i = 0; i < arr.length; i++) {
const element = arr[i];
console.log(element);
}
// for in 문. iterates over all enumerable properties of an object that are keyed by strings
for (const key in arr) {
if (Object.hasOwnProperty.call(arr, key)) {
const element = arr[key];
console.log(element);
}
}
// for of 문. loop iterating over iterable objects,
// iterable을 받아서 [Symbol.iterator] 메서드를 실행한 후 {next, value}에서 value를 연달아 반환
for (const value of arr) {
console.log(value);
}
음 좋다. for...of에서는 iterable을 순회한다는 것을 알았다.
이 iterable에 속하는 것으로, String, Array, 유사 배열, TypedArray, Map, Set, 그리고 유저가 직접 정의한 iterable가 속한다.
String, Array와 같이 애초에 iterable인 객체들은 built-in iterable이라고 부른다.
여튼, 그래서 iterable이 아닌 단순 객체를 for...of에 사용하려고 하면 iterable이 아니라는 에러가 발생합니다.
그렇다면 iterable은 어떤 것이길래, object는 iterable이 아닌 것인가? 여기에는 명확한 정의가 있다.
iteration => iterable, iterator
우선, iterable과 iterator를 합쳐서 iteration이라고 부르는데
여기에 해당하는 문서와 조건(프로토콜)에 대해서는 아래 문서를 참고해야 한다.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols
위 문서를 정리하자면, 아래와 같다. (당연히, 요약은 정확하지 않으니 명확한 정의는 직접 문서를 봐야 함)
- iterable의 조건 : Symbol.iterator 메서드를 가지고 있는 객체. 정확히는 @@iterator 메소드를 구현하는 객체.
- Symbol.iterator는 iterator 객체를 반환함.
- iterator의 조건 : next 메서드를 가지고 있음
- next 메서드는 { value, done } 꼴의 객체를 반환함.
그렇다면, built-in iterable인 array에도 Symbol.iterator 메서드가 구현되어 있을 것이라고 예측할 수 있습니다.
const arr = [1, 2, 3];
console.log(arr[Symbol.iterator]); // [Function: values]
iterator를 반환하지 못하도록, Symbol.iterator 메서드를 null로 지워버리면, arr가 iterable이 아니라는 에러 메세지를 확인할 수 있습니다.
// 브라우저 환경에서만 에러가 남. node 환경에선 에러 안 남.
const arr = [1, 2, 3];
arr[Symbol.iterator] = null;
for (const value of arr) {
console.log(value); // Error! arr is not iterable
}
Symbol.iterator 메서드가 iterator를 반환하니 아래처럼 iterator를 꺼낸 후에 next()를 호출할 수도 있겠죠.
const arr = [1, 2, 3]; // iterable
const iterator = arr[Symbol.iterator]();
console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 2, done: false }
console.log(iterator.next()); // { value: 3, done: false }
console.log(iterator.next()); // { value: undefined, done: true }
next로 원소 하나를 소비 한 후에 for...of를 돌릴 수도 있습니다.
이유는, Symbol.iterator에 의해 반환된 iterator 내부에 또 Symbole.itertor 메서드가 존재하기 때문입니다.
즉, iterator이면서 동시에 iterable일수도 있다는 것입니다! 이렇게 구성된 iterator를 well-formed iterable라고 부릅니다.
const arr = [1, 2, 3]; // iterable
const iterator = arr[Symbol.iterator]();
iterator.next();
for (const a of iterator) {
console.log(a);
}
built-in iterable인 Map도 아래처럼 for...of를 사용할 수 있겠죠.
const map = new Map([
['a', 1],
['b', 2],
]);
for (const [key, value] of map) {
console.log(key, value);
}
// map.keys()의 결과물도 결국 iterable이기 때문에 for...of를 돌릴 수 있음
for (const key of map.keys()) {
console.log(key);
}
custom iterable 제작하기 + well formed iterable
iterable, iterator의 프로토콜만 지킨다면, 아래와 같은 꼴의 iterable을 스스로 만들어볼 수도 있겠죠.
생각보다 간단합니다!
const customIterable = {
// Symbol.iterator 메서드는 iterator를 반환해야 함
[Symbol.iterator]() {
let i = 0;
return {
// iterator는 next 메서드가 있어야 하며 {value, done} 이 필요함.
next() {
if (i > 3) return { value: i, done: true };
i++;
return { value: i, done: false };
},
};
},
};
for (const i of customIterable) {
console.log(i);
}
그런데 위 custom iterable은 for...of 문으로 동작하긴 하지만, 중간에 iterator를 추출한 후에 다시 for문으로 돌리기 위해서
iterator인 동시에 iterable인 것이 좋습니다. 즉, well-formed iterable여야 한다는 것이죠.
위 customIterable을 well-formed iterable로 만들어소 중간에 iterator의 next로 한 번 끊고 for...of를 적용할 수 있도록 만듭시다.
const customIterable = {
// Symbol.iterator 메서드는 iterator를 반환해야 함
[Symbol.iterator]() {
let i = 0;
return {
// iterator는 next 메서드가 있어야 하며 {value, done} 이 필요함.
next() {
if (i > 3) return { value: i, done: true };
i++;
return { value: i, done: false };
},
// iterator이자 iterable인 well-formed iterable
[Symbol.iterator]() {
return this; // 자기 자신을 반환
},
};
},
};
const iter = customIterable[Symbol.iterator]();
console.log(iter.next());
for (const i of iter) {
console.log(i);
}
여담
순회할 수 있는 객체들은, iterable, iterator, well-formed iterable의 프로토콜을 잘 따르는 것이 좋다.
심지어 전개 연산자도 이러한 프로토콜을 잘 따르고 있다. 패키지 만들거면 이런 점들을 잘 고려해서 만들도록 하자.
MDN 문서 ref)
iteration과 iterable, iterator 프로토콜 : https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols
for...of : https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...of
for...in : https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...in
'Programming Language > 🟨 Javascript (Core)' 카테고리의 다른 글
__proto__ vs prototype (0) | 2021.06.10 |
---|---|
javascript 모듈 시스템 : CJS, AMD, UMD, ESM (0) | 2021.05.30 |
Reflect, Proxy (1) : Reflective 프로그래밍을 위한 표준 내장 객체들 (0) | 2021.04.16 |
TS로 살펴보는 Promise + Promise.all 병렬처리 (0) | 2020.08.25 |
Symbol 타입의 변수 (0) | 2020.07.15 |