이번 글에는 Iterator와 Generator의 스펙과, 왜 등장했는지, 어떻게 사용하는지에 대해서 정리해본다.
Iterator
Iterator는 ES6에 추가되었다. 다시 해석하면 그 전에도 어찌어찌 구현하던 것을 Iterator로 바꿀 수 있단 뜻이다.
Iterator는 정의된 인터페이스를 구현하면 그 구현체를 순회할 수 있는 객체이다.
순회라고 하면 Array가 떠오르고, Array를 순회하려면 forEach
나 for-in
으로도 가능하다.
하지만 forEach
는 순회 중간에 중단할 수 없는 단점이 있고, for-in
은 이름부터 원하는 순회를 하는 듯 하지만 역시 아래와 같은 여러 단점이 있다.
for (const i in arr)
에서i
의 type이 string으로 뭔가 이상하다.- 왜 string가 하니
arr
에 추가 속성이 있는 경우i
가 추가 속성까지 순회한다. - 그마저도 순서가 보장되지 않는다.
이런 문제가 있어서 for-of
가 등장했다. for-of
는 for-in
의 단점을 보완하고 순회하는 대상을 Array에서 더 나아가 Iterator를 지원한다.
이제 Iterator가 나왔다. Iterator를 제공하는 것은 뭔지 그 구현체를 보자.
const countIterator = {
[Symbol.iterator]: function () {
return this;
},
cnt: 0,
next: function () {
return {done: false, value: ++this.cnt};
}
};
Iterator는 [Symbol.iterator]()
를 시작으로 next()
method가 정의된다. next()
는 done
과 value
속성을 가진다. done
은 boolean type으로 해당 Iterator는 true
가 나올때 까지 반복된다.
위 countIterator
는 항상 {done: false}
를 반환하기 때문에 무한 반복된다.
첫 예제부터 Array로는 어떻게 구현해야 할지 어려운 무한 반복되는 Iterator를 구현했다. 뭔가 대단하다.
무한반복이 아닌 10까지 카운트하는 동작을 원하면 원하는 시점에 {done: true}
를 반환하도록 next를 수정하면 된다.
next: function () {
return {done: this.cnt >= 10, value: ++this.cnt};
}
{done: true}
가 나올 때까지 계속 next를 호출해주는 for-of
에서 돌려보면 10까지 출력하는 것을 확인할 수 있다.
for(const cnt of countIterator) {
console.log(cnt); // 1 2 3 4 5 6 7 8 9 10
}
Iterator를 살펴봤는데 가장 중요한 점은 위 카운터 예제에서도 알 수 있지만 next()
가 수행되기 전까지 현재의 context(여기서는 cnt)를 유지한다는 점이다. context를 유지하며 실행/중지를 반복하여 Lazy Evaluation을 가능하게 하며 이 점을 이용해서 더 멋진 코딩을 할 수 있다.
Generator
Generator는 함수와 비슷하게 정의하지만 호출하면 Iterator 객체가 반환되어 next()
method로 제어하게 된다.
Iterator가 반환된다는 것은 [Symbol.iterator]()
와 next()
가 구현되었다는 뜻이고, 이걸 javascript가 변환해주려면 당연히 Generator를 문법에 맞게 선언해야 한다.
function* countGenerator(max) {
for (let i = 1; i <= max; i++)
yield i;
}
위에 Iterator로 구현한 카운터와 똑같은데 Generator로 구현하니 훨씬 간결해졌다.
Iterator로 변환되기 위한 Generator의 문법은 다음과 같다.
function*
로 시작한다.yield
로next()
의value
를 대신한다. 이때done
은false
이다.return x
로next()
의{value: x, done: true}
를 대신한다.
함수의 종료는 return undefined
와 같기 때문에 3번 조건을 위해 별도의 코딩은 없어도 된다.
따라서 위에 직관적인 3줄의 코드로 Generator문법으로 Iterator를 만들었으니 실제 사용할 때도 Iterator와 같은 방법으로 쓰면 된다.
const counter10 = countGenerator(10);
for(const cnt of counter10) {
console.log(cnt); // 1 2 3 4 5 6 7 8 9 10
}
Generator로 구현했지만 역시 context를 유지하는 Iterator로 변환된 것을 확인할 수 있다.
yield의 추가 기능
yield
는 next().value
를 대신해주는 것 외에도 추가 기능이 있다.
next()
로yield
의 반환 값을 넘길 수 있다.
function* countGenerator(max) {
for (let i = 1; i <= max; i++) {
const decrease = yield i;
if(decrease === true) i -= 2;
}
}
const counter10 = countGenerator(10);
counter10.next(); // {value: 1, done: false}
counter10.next(); // {value: 2, done: false}
counter10.next(true); // {value: 1, done: false}
yield*
로 또 다른 Iterator/Generator를 실행시킬 수 있다.
function* concatGenerator(counterA, counterB){
yield* counterA;
yield* counterB;
}
const counter3 = countGenerator(3);
const counter5 = countGenerator(5);
const concatCounter = concatGenerator(counter3, counter5);
for(const value of concatCounter) {
console.log(value); // 1 2 3 1 2 3 4 5
}
Generator를 이용한 Permutation 구현
위에서 익힌 Generator와 yield*를 이용해서 permutation도 만들 수 있다.
function permutation(arr) {
const n = arr.length, visited = Array(n), result = Array(n);
return function* _permutation(cur) {
for(let i = 0; i < n; i++) {
if(visited[i]) continue;
visited[i] = true;
result[cur] = arr[i];
yield* _permutation(cur + 1);
visited[i] = false;
}
if(cur == n) yield result;
}(0);
}
for(const perm of permutation([1,2,3])) {
console.log(perm)
}
/*
[1, 2, 3]
[1, 3, 2]
[2, 1, 3]
[2, 3, 1]
[3, 1, 2]
[3, 2, 1]
*/
'javascript' 카테고리의 다른 글
React class vs function component 차이점 (0) | 2020.05.17 |
---|---|
Typescript 타입 정의 파일 (0) | 2020.03.29 |
CommonJS / ES 모듈 로딩 방식 (1) | 2020.03.29 |
ES8의 async와 await (0) | 2019.10.16 |
ES6의 Promise (0) | 2019.10.16 |
댓글