Iterator(반복자)
는 반복 처리가 가능한 객체를 의미합니다. 좀 더 쉽게 이야기 하자면, 열거 가능한 객체들이 각 요소에 대해 반복 처리가 가능하죠? 이런 열거 가능한 객체들을 반복자라고 합니다. 대표적으로 Array, String, Set, Map 등이 반복자를 가지고 있습니다. 반복자를 가지고 있는 객체들의 특징은 for ~ of
문을 통해 요소의 열거가 가능하다는 점 입니다.
Iterator
는 next()
메소드를 갖습니다. 이 메소드는 반환값으로 done과 value를 가지고 있는 객체를 반환합니다.
done
은 객체의 반복이 끝났는지에 대한 boolean 형 값을 의미합니다. 반복이 끝났다면 done은 true를 가지고, 끝나지 않았다면 false를 갖습니다.
value
는 객체로부터 현재 가져온 값을 반환합니다.
let arr = [1, 2, 3];
//[Symbol.iterator]() 메소드는 반복자를 반환합니다.
let itr = arr[Symbol.iterator]();
let elem;
while(elem = itr.next()) {
if (elem.done) {
break;
}
console.log(elem.done);
console.log(elem.value);
}
[Symbol.iterator]
라는 구문이 보이는데, 이 구문은 Iterator 객체를 반환해줍니다. 이 객체가 next 메소드를 가지고 있으며, 이 메소드를 통해 반복자 객체의 값을 활용할 수 있습니다. 그리고 이 중에서 값을 쉽게 취득할 수 있도록 생긴 구문이 바로 for ~ of
문인 것 입니다. 그래서 아까 위에서 반복자를 가진 객체들은 for ~ of
를 사용할 수 있다라고 했었던 것 입니다.
[Symbol.iterator]
에서 프로퍼티에 대괄호[]
가 온 것이 무슨 문법인가 하시는 분들이 계실텐데요. 이 문법은 객체 확장 표현식에서 다룬 계산된 프로퍼티(computed property names)구문입니다. 복습하자면, 대괄호 내에 자바스크립트 표현식을 넣으면, 해당 표현식이 프로퍼티의 키로 사용될 수 잇게 해주는 것 입니다. 여기서는 Symbol.iterator
가 하나의 프로퍼티 키로 사용되었음을 의미합니다.
사용자 정의 객체에 대해서도 Iterator
로 만들 수 있습니다. 방금 전에 본 [Symbol.iterator]
를 정의하기만 한다면 사용자 정의 객체도 반복자로 만들 수 있습니다.
class HandmadeItr {
constructor(data) {
this.data = data;
}
//반복자 정의
[Symbol.iterator]() {
let current = 0;
let that = this;
return {
next() {
return current < that.data.length ?
{value: that.data[current++], done: false} : {done: true};
}
};
}
}
let itr = new HandmadeItr([1, 2, 3]);
for (let value of itr) {
console.log(value);
}
사용자 정의 객체인데도 불구하고 for ~ of
문이 잘 작동하죠? 제대로 반복자가 되었음을 알 수 있습니다.
위 코드에서 다음 부분이 반복자를 정의하는 코드입니다.
[Symbol.iterator]() {
let current = 0;
let that = this;
return {
next() {
return current < that.data.length ?
{value: that.data[current++], done: false} : {done: true};
}
};
}
여기서 주목할 것은 that
입니다. 왜 this를 따로 that
에 저장하고 사용했냐면, 메소드 내부의 this는 호출한 객체를 가리킵니다. 따라서 next 메소드 내부에서 this를 사용하면 [Symbol.iterator]
를 가리키게됩니다. 하지만 우리에게 필요한 건 this가 우리가 만든 객체를 가리켜야합니다.
그렇기 떄문에 [Symbol.iterator]
에서 this를 쓰면 우리가 만든 객체를 가리키므로, next 메소드에 들어가기전에 this를 호출해서 따로 that
이라는 이름으로 따로 담아두고 사용하는 것 입니다. 이렇게 하면 next 메소드 내부에서 객체에 접근할 수 있게 됩니다.
그리고 마지막에 반복자는 next 메소드를 반환하는데, 이 next 메소드는 또 done과 value로 이루어진 객체를 반환합니다. 위에서 next 메소드의 반환값이 done과 value로 이루어진 객체라고 했던 것 기억하시죠?
이렇게 사용자 정의 객체에 반복자를 직접 정의함으로써 사용자 정의 객체를 반복자 객체로 만들 수 있게됩니다.
바로 위에서 사용자 정의 객체를 반복자로 만드는 것을 보았는데, 솔직히 복잡하죠? 그래서 반복자 객체를 쉽게 만들기 위해 Generator(발생자)
가 등장했습니다.
Generator
는 반복자 객체를 쉽게 만들 수 있게 해줍니다. Generator
는 다음과 같이 생겼습니다.
function* gen() {
yield 'Ge';
yield 'nera';
yield 'tor'
}
for (let value of gen()) {
console.log(value);
}
function*
과 yield
라는 것이 등장했는데, 이 두 가지에 대해서 알아보겠습니다.
function*
은 발생자 정의를 위한 함수 키워드입니다. *
를 반드시 붙여야 이 함수가 발생자 선언을 한다고 스크립트가 해석할 수 있으니 잊으시면 안됩니다.
yield
는 발생자에서 값을 반환하는 명령입니다. 근데 함수에서 값을 반환하는 것으로는 return
도 있고 그동안 return
을 사용해왔습니다. 두 명령의 차이는 무엇일까요?
reutrn
은 값을 반환하고 함수를 종료시킵니다. 그래서 재호출하는 경우 함수가 처음부터 다시 실행됩니다. 반면 yield
는 값을 반환하고 함수를 일시정지시킵니다. 그래서 함수를 재호출하면 정지했던 위치 다음 줄부터 함수가 다시 실행이 됩니다. 그래서 위의 예시에서 Ge Ge Ge
가 아니라 Ge nera tor
이라고 출력이 된 것 입니다.
그러면, 발생자를 이용해서 사용자 정의 객체의 반복자를 정의해보겠습니다.
class HandmadeItr {
constructor(data) {
this.data = data;
this[Symbol.iterator] = function* () {
let current = 0;
let that = this;
while(current < that.data.length) {
yield that.data[current++];
}
}
}
}
let itr = new HandmadeItr([1, 2, 3]);
for (let value of itr) {
console.log(value);
}