
자바스크립트 완벽가이드 12장에 해당하는 부분이고, 읽으면서 자바스크립트에 대해 새롭게 알게된 부분만 정리한 내용입니다.
이 장에서는 이터레이터가 어떻게 동작하는지 설명하고 이터러블 데이터 구조를 직접 만드는 방법을 설명한다.
JS의 순회를 이해하려면 다음 3가지를 이해해야 한다.
Symbol.iterator라는 이터레이터 메서드를 가진 객체
next()메서드가 있는 객체
value와done프로퍼티가 있는 객체
즉 다시 말하면, 이터러블 객체란 이터레이터 객체를 반환하는 특별한 이터레이터 메서드(Symbol.iterator)를 가진 객체이다. 또한 이터레이터 객체는 순회 결과 객체를 반환하는 next() 메서드가 있는 객체이다.
이터러블 객체 iterable을 순회하는 단순한 for/of 루프 예시
// 객체 생성
const iterable = [99];
// 이터레이터 메서드를 호출하여 이터레이터 객체 생성
const iterator = iterable[Symbol.iterator]();
for (let result = iterator.next(); !result.done; result = iterator.next()) {
console.log(result.value); // 99
}
이터레이터 객체 그 자체가 이터러블인 경우
const list = [1, 2, 3, 4, 5];
const iter = list[Symbol.iterator]();
const head = iter.next().value;
const tail = [...iter];
console.log(head); // 1
console.log(tail); // [2, 3, 4, 5]
내장된 이터러블 데이터 타입의 이터레이터 객체는 그 자체가 이터러블이다. 이런 특징이 유용할 때가 간혹 있다.
클래스를 이터러블로 만들기 위해서는 반드시 이름이 Symbol.iterator인 메서드를 만들어야 한다. 이 메서드는 반드시 next() 메서드가 있는 이터레이터 객체를 반환해야한다. next() 메서드는 반드시 순회 결과 객체를 반환해야 하며 순회 결과 객체에는 value 프로퍼티와 불 done 프로퍼티 중 하나는 반드시 존재해야 한다.
class Range {
constructor(from, to) {
this.from = from;
this.to = to;
}
has(x) {
return typeof x === 'number' && this.from <= x && x <= this.to;
}
toString() {
return `{x | ${this.from} <= x ${this.to}}`;
}
// 이터레이터 객체 반환 -> 이터러블을 만들기 위해서
[Symbol.iterator]() {
let next = Math.ceil(this.from);
const last = this.to;
return {
// next() 메서드가 이터레이터 객체의 핵심
next() {
return next <= last ? { value: next++ } : { done: true };
},
[Symbol.iterator]() {
return this;
}
};
}
}
for (const x of new Range(1, 10)) console.log(x); // 1부터 10까지 숫자
console.log(...new Range(-2, 2)); // -2 -1 0 1 2
이터러블 객체와 이터레이터 핵심 특징 중 하나는 이들이 본질적으로 느긋하다(lazy)는 것이다. 따라서 그 값이 실제 필요할 때까지 계산을 늦추어 메모리를 아낄 수 있다.
이터레이터 객체에 종료를 위해 return() 메서드가 사용되기도 한다.
next()가 done 프로퍼티가 true인 순회 결과를 반환하기 전에 순회를 마쳐야 한다면 인터프리터는 이터레이터 객체에
return()메서드가 있는지 확인한다.
function* 키워드를 사용하여 정의한다. 이 함수를 호출하면 제너레이터 객체를 반환한다.
function* oneDigitPrimes() {
yield 2;
yield 3;
yield 5;
yield 7;
}
// 제너레이터 함수를 호출하여 제너레이터를 생성한다.
const primes = oneDigitPrimes();
console.log(primes.next().value); // 2
console.log(primes.next().value); // 3
console.log(primes.next().value); // 5
console.log(primes.next().value); // 7
console.log(primes.next().done); // true
// 제너레이터는 다른 이터러블 타입처럼 사용할 수 있다.
console.log([...oneDigitPrimes()]); // [2, 3, 5, 7]
표현식으로 제너레이터 정의는 가능하지만 화살표 함수 문법은 불가능하다. 또한 제너레이터를 사용하면 아래와 같이 이터러블 클래스를 만들기 쉽다.
// Range 클래스에서 제너레이터를 사용하지 않은 경우
// 이터레이터 객체 반환 -> 이터러블을 만들기 위해서
[Symbol.iterator]() {
let next = Math.ceil(this.from);
const last = this.to;
return {
// next() 메서드가 이터레이터 객체의 핵심
next() {
return next <= last ? { value: next++ } : { done: true };
},
[Symbol.iterator]() {
return this;
}
};
}
// Range 클래스에서 제너레이터를 시용한 경우
*[Symbol.iterator]() {
for(let x = Math.ceil(this.from); x <= this.to; x++) yield x;
}
제너레이터를 활요한 피보나치 수열
function* fibonacciSequence() {
let x = 0;
let y = 1;
for (;;) {
yield y;
[x, y] = [y, x + y];
}
}
function fibonacci(n) {
for (const f of fibonacciSequence()) {
if (n-- <= 0) return f;
}
}
console.log(fibonacci(20)); // 10946
// 무한한 제너레이터를 take() 제너레이터와 함께 사용한 경우
function* take(n, iterable) {
// 이터레이터 객체 생성
const it = iterable[Symbol.iterator]();
while (n-- > 0) {
const next = it.next();
if (next.done) return;
yield next.value;
}
}
console.log([...take(5, fibonacciSequence())]); // 1 1 2 3 5
yield* 키워드는 yield와 비슷하지만 값 하나를 전달하는 것이 아니라 이터러블 객체를 순회하면서 각각의 값을 전달한다.
function* oneDigitPrimes() {
yield 2;
yield 3;
yield 5;
yield 7;
}
function* sequence(...iterables) {
for (const iterable of iterables) {
yield* iterable;
}
}
console.log([...sequence('abc', oneDigitPrimes())]); // ['a', 'b', 'c', 2, 3, 5, 7]
하지만 아래처럼배열요소를 순회하기 위해 forEach() 메서드를 사용하는 경우 정상적으로 작동이 되지 않는다.
function* sequence(...iterables) {
iterables.forEach(iterable => yield* iterable); // 에러
}
위 예제의 중첩된 화살표 함수는 일반적인 함수이므로
yieldyield*는 제너레이터 함수 안에서만 사용할 수 있으므로 허용되지 않는다.
yield*을 사용해 재귀 제너레이터를 만들수 있고, 재귀적으로 정의된 트리구조에 비재귀적 순회를 수행할 수 있다.
제너레이터 함수도 다른 함수와 마찬가지로 값을 반환할 수 있다.
function* oneAndDone() {
yield 1;
return 'done';
}
console.log([...oneAndDone()]);
const generator = oneAndDone();
console.log(generator.next()); // { value: 1, done: false }
console.log(generator.next()); // { value: 'done', done: true }
console.log(generator.next()); // { value: undefined, done: true }
next()를 마지막으로 호출했을 때 반환하는 객체에는 value와 done이 모두 존재한다.
yield는 표현식이라서 값을 가질 수 있다.
function* smallNumbers() {
console.log('next()가 처음 호출되었으며 인자는 무시됩니다.');
const y1 = yield 1; // y1 === 'b'
console.log(`next()가 두 번째로 호출됐으며 인자는 ${y1}입니다.`);
const y2 = yield 2; // y2 === 'c'
console.log(`next()가 두 번째로 호출됐으며 인자는 ${y2}입니다.`);
const y3 = yield 3; // y3 === 'd'
console.log(`next()가 두 번째로 호출됐으며 인자는 ${y3}입니다.`);
return 4;
}
const g = smallNumbers();
console.log('제너레이터가 생성됐습니다. 아직 실행된 코드는 없습니다.');
const n1 = g.next('a'); // n1.value = 1;
console.log(`제너레이터가 전달한 값은 ${n1.value}입니다.`);
const n2 = g.next('b'); // n2.value = 2;
console.log(`제너레이터가 전달한 값은 ${n2.value}입니다.`);
const n3 = g.next('c'); // n3.value = 3;
console.log(`제너레이터가 전달한 값은 ${n3.value}입니다.`);
const n4 = g.next('d'); // n4 === {value: 4, done: true}
console.log(`제너레이터는 ${n4.value}를 넘기고 종료됐습니다.`);
제너레이터의
next()메서드를 다음에 호출할 때next()에 전달된 인자는 멈췄던 yield 표현식의 값이 된다.
즉, 호출자는next()를 통해 제너레이터에 값을 전달한다. 첫 번째 전달 값은 무시된다.
next()뿐만 아니라 return()과 throw() 메서드를 호출해서 제너레이터의 실행 흐름을 제어할 수 있다.
제너레이터에서는 try/finally 문을 통해 제너레이터가 종료될 때(finally 블록에서) return()을 사용하여 필요한 정리 작업을 수행하게 만들 수 있다. throw()도 마찬가지로 임의의 신호를 예외의 형태로 제너레이터에 보내 예외 처리를 할 수 있다.
제너레이터가 yield*를 통해 다른 이터러블 객체에 값을 전달하면 제너레이터의 next() 메서드를 호출할 때 이터러블 객체의 next() 메서드가 호출된다. return()과 throw() 메서드도 마찬가지이다. 제너레이터가 return()과 throw() 메서드가 정의된 이터러블 객체에 yield*를 사용하면, 제너레이터에서 return()이나 throw()를 호출할 때 이터레이터의 return()이나 throw() 메서드가 이어서 호출된다.