먼저 간단하게 요약해보자면 다음과 같다 :
next() 메서드를 갖고 있는 객체[Symbol.iterator]() 메서드를 갖고 있는 객체Iterable Iterator 객체function*으로 시작하는 함수Iterator
next()메서드를 갖고 있는객체
const myIterator = {
next() {
return { value: 0, done: false };
},
};
next() 메서드는 객체를 돌려줘야 한다.
이 객체는 value와 done 프로퍼티를 가져야 한다.
value = 돌려줄 값
done = iteration이 끝났는지 여부
{ value: undefined, done: false }
만약 프로퍼티에 값이 없다면, 각각 이 값이 적용된다고 한다.
정리하면, "위와 같은 객체를 돌려주는
next()메서드"를 가진객체를Iterator라고 한다.
Iterable
[Symbol.iterator]()메서드를 갖고 있는객체
[Symbol.iterator]() = [@@iterator]() = @@iterator method 모두 같은 말이다.
코드에서 쓸 때는 Symbol.iterator를 쓴다.
const myIterator = {
next() {
return { value: 0, done: false };
},
};
const myIterable = {
[Symbol.iterator]() {
return myIterator;
},
};
[Symbol.iterator]() 메서드는 Iterator 객체를 돌려줘야 한다.
따라서 정리해보면 다음과 같다.
"
Iterator를 돌려주는[Symbol.iterator]()메서드"를 갖고 있는 객체를 Iterable이라고 한다.
구체적인 예시로 알아보면 이렇다.
const myIterator = {
i: 0,
next() {
return this.i < 3
? { value: this.i++, done: false }
: { value: undefined, done: true };
},
};
const myIterable = {
[Symbol.iterator]() {
return myIterator;
},
};
myIterator는 i가 3보다 작을 때 value로 i를 돌려주면서 1을 더하고, i가 3이 되면 끝난다.
for (let k of myIterable) {
console.log(k);
}
// 0
// 1
// 2
spread syntax, for...of 같은 자바스크립트의 많은 문법들은 Iterable을 받는다.
for...of 루프는 Iterable의 [Symbol.iterator]() 메서드를 부르면서 시작한다.
그리고 그 메서드로부터 받은 Iterator 객체로 루프를 돈다.
각 반복마다 그 Iterator의 next() 메서드를 불러서 값을 얻는거다.
Iterable Iterator방금까지의 내용을 정리해보면 다음과 같다 :
next() 메서드를 갖는 객체가 Iterator고, [Symbol.iterator]() 메서드를 갖는 객체가 Iterable이다.
따라서 객체는 Iterator이면서 동시에 Iterable일 수 있다. 그런 객체를 Iterable Iterator라고 이름 붙여보자.
자바스크립트의 여러 문법들은 Iterable을 받으므로, 다음처럼 Iterable Iterator를 쓰면 복잡해질 필요 없이 하나의 객체로 해결할 수 있을 것이다 :
const myIterableIterator = {
i: 0,
next() {
return this.i < 3
? { value: this.i++, done: false }
: { value: undefined, done: true };
},
[Symbol.iterator]() {
return this;
},
};
for (let k of myIterableIterator) {
console.log(k);
}
// 0
// 1
// 2
따라서 Iterable이 아닌 Iterator는 별로 유용하지 않다고 볼 수 있다.
Iterator를 만든다면 [Symbol.iterator]() 메서드도 구현해주는 게 훨씬 쓸모있다.
Generator제너레이터 함수가 돌려주는
Iterable Iterator객체
제너레이터는 Iterator에 속한다.
즉, next() 메서드를 갖는 객체이다.
그리고 Iterable이기도 하다.
즉 [Symbol.iterator]() 메서드도 갖는데, 제너레이터 객체 자기 자신을 돌려준다.
따라서 Iterable Iterator라고 할 수 있다.
그리고 제너레이터는 Generator Function을 통해서만 생성된다.
Generator Function
function*으로 시작하는함수
function* myGeneratorFunction() {
yield 0;
yield 1;
yield 2;
}
const myGenerator = myGeneratorFunction();
제너레이터 함수는, 불러지면 일반 함수처럼 내부가 실행되는 게 아니라 Generator 객체를 돌려준다.
참고로 Generator 객체는, 앞서 적었듯 next()를 갖는 Iterator이다.
그리고 그 Generator에서 next()가 불러졌을 때, 제너레이터 함수의 내부가 실행된다.
이 실행은 처음으로 만나는 yield expression까지 계속된다.
yield는 Generator의 next()가 돌려줄 값을 결정한다.
정리해보면 이렇다.
제너레이터 함수의 내부는 자신이 돌려준 Generator의
next()를 부를 때마다 실행되는데,yield를 만날 때까지만 실행된다.
function* myGeneratorFunction() {
console.log("a");
yield 0;
console.log("b");
yield 1;
console.log("c");
yield 2;
console.log("d");
}
const myGenerator = myGeneratorFunction();
console.log(myGenerator.next());
// a
// {value: 0, done: false}
console.log(myGenerator.next());
// b
// {value: 1, done: false}
다르게 말해, yield는 제너레이터 함수 내부의 실행을 멈추고 재개하는 지점이 된다.
그리고 yield operator 뒤에 오는 expression의 값이, next()가 돌려주는 객체의 value가 된다.
function addOne(num) {
return num + 1;
}
function* myGenFunc() {
yield addOne(1) + (2 * 3) / 4;
}
const myGen = myGenFunc();
console.log(myGen.next());
// {value: 3.5, done: false}
따라서 제너레이터 함수는 매우 유용하다.
불렀을 때 만들어서 돌려주는 Generator는 Iterator이자 Iterable이며,
그 Generator가 반복될 때마다 돌려줄 값(value)은 제너레이터 함수 내부에서 yield로 지정해줄 수 있다.
결론적으로, 제너레이터 함수를 쓰면 지금까지 나온 모든 걸 한 번에 할 수 있다.
Iterator와 Iterable을 알아야 어떻게 돌아가는지 이해할 수 있긴 하지만, 실질적으로 사용하는 건 Generator Function이라는 걸 알 수 있다.
for (let i of range(0, 3)) {
console.log(i);
}
이렇게 사용할 수 있는 range() 함수를 Iterable Iterator와 Generator function으로 구현해보자.
// Iterable Iterator를 돌려주는 함수
function range(start, stop) {
let current = start;
return {
next() {
return current < stop
? { value: current++, done: false }
: { value: undefined, done: true };
},
[Symbol.iterator]() {
return this;
},
};
}
for (let i of range(0, 3)) {
console.log(i);
}
// 0
// 1
// 2
// Generator를 돌려주는 제너레이터 함수
function* range(start, stop) {
for (let i = start; i < stop; i++) yield i;
}
for (let i of range(0, 3)) {
console.log(i);
}
// 0
// 1
// 2
제너레이터 함수를 쓰면 코드가 더 짧기도 하지만, 복잡하게 세세한 걸 적을 필요 없이 돌려줄 값만 적으면 된다는 점도 크다.
function* zeroesForever() {
while (true) yield 0;
}
const zeroGenerator = zeroesForever();
console.log(zeroGenerator.next().value);
// 0
console.log(zeroGenerator.next().value);
// 0
console.log(zeroGenerator.next().value);
// 0
길이가 무한인 배열을 만들 수는 없지만, 이렇게 끝나지 않는 제너레이터는 만들 수 있다.
사용자가 필요한만큼 사용할 수 있다.
수정 : 2025-01-24