[Javascript] Iterator(반복자), Generator(발생자)
오늘 소개드릴 자바스크립트 문법은 반복자와 발생자 입니다. 두 문법 모두 es2015에 추가된 문법입니다.
1. 반복자 Iterator
Iterator는 반복된 처리가 가능한 객체를 의미합니다. 다른 말로는 객체 내부의 내용을 열거가 가능해서 각 요소들에 대해서 반복처리가 가능한 것들을 의미합니다. 대표적으로 Array, String, Set, Map 등이 반복자를 가지고있는 객체입니다.반복자를 가지고있는 객체들은 보통 다음과 같이 for~of문을 통해서 요소를 열거할 수 있습니다.
ES2015부터 추가된 이터레이터 객체는 next()라는 메소드를 갖습니다. 이 메소드의 반환값으로는 done과 value를 가진 객체를 반환합니다. done은 반복이 끝났는지(끝났다면 true, 끝나지 않았다면 false)에 대한 boolean형 값이 반환되고, 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문을 통해서 편리하게 value만 쉽게 취득하고 이용할 수 있게 만들어진 것 입니다.
객체의 프로퍼티에 대괄호([])가 온 것은 computed property names라는 구문입니다. 대괄호 안에 어떤 표현식을 넣으면 그 표현식이 프로퍼티 명으로 사용될 수 있게 해주는 구문입니다. 즉 Symbol.iterator가 하나의 프로퍼티인 것 입니다.
2. 직접만든 객체에 Iterator 적용하기
그러면 위에서 알아본대로 우리가 만든 객체에 [Symbol.iterator]를 정의하기만 한다면 내장 객체말고도 직접 만든 객체에도 이터레이터가 적용이 되는지 직접 만들어보겠습니다.
반복자를 정의할 때 [Symbol.iterator]는 next()라는 메소드를 가지고 있으며, 반환은 { done: boolean, value: any }라는 객체를 반환해야한다는 점을 기억해주세요.
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};
}
};
}
이 부분이 반복자를 만들기 위한 부분입니다. current는 데이터에서 현재 위치를 의미합니다.
that이 조금 난해한데, that은 객체 자기자신(HandmadeItr 객체)을 의미합니다. 왜냐하면 자바스크립트의 this는 메소드 내부에서 사용할 시 호출한 객체를 가리키게 됩니다. 그래서 next()에서 this를 사용하면 [Symbol.iterator] 반복자를 가리키게 됩니다. 하지만 우리가 활용할 것은 객체의 데이터 입니다. 그래서 next()로 진입하기 전에 this를 사용하면 [Symbol.iterator]가 속한 객체(HandmadeItr)을 가리키게 되므로 사전에 미리 that이라는 이름으로 따로 저장해두고 사용하면 next() 메소드 내부에서 객체에 접근할 수 있게 됩니다.
반환하는 next() 메소드는 객체를 반환합니다. 이때 현재 위치(current)가 객체의 데이터 길이보다 작으면 {value, done} 객체를, 현재 위치가 데이터보다 길어지면 done을 반납해서 반복을 종료시킵니다.
3. 발생자 Generator
class 명령의 생성자(constructor)와의 혼용을 방지하기 위해서 생성기 대신 발생자라는 용어를 채택했습니다.
위에서 직접 반복자 객체를 만들었는데, 솔직히 코드가 난해하기도 하고, 복잡하다는 감이 있습니다. 그래서 이런 반복자 객체를 쉽게 만들어주기 위한 것이 등장했는데, 그것이 바로 발생자(Generator)입니다.
발생자는 반복자 객체를 간단하게 만들 수 있도록 만들어줍니다. 발생자는 다음과 같이 생겼습니다.
function* gen() {
yield 'Ge';
yield 'nera';
yield 'tor'
}
for (let value of gen()) {
console.log(value);
}
눈에 띄는게 두가지가 있습니다. function*과 yield 입니다.
function*은 발생자를 정의하기 위한 함수 키워드 입니다. *를 잊지말고 붙여야 발생자를 선언하기 위한 키워드임을 알 수 있습니다.
yield는 발생자에서 값을 반환합니다. return도 값을 반환하는 것인데, return은 만나게 되면 값을 반환하고 함수를 종료합니다. 그래서 yield 대신 return을 넣으면 반복할 때마다 함수가 종료되었으므로 항상 처음부터 다시 시작해 'Ge Ge Ge'만을 출력하게 됩니다. 반면 yield는 값을 반환하지만 종료시키지 않고 일시 정지 시킵니다. 그래서 다시 실행되면 이전 yield 이후로부터 다시 출력이 되어 'Ge nera tor'가 출력이 된 것 입니다.
그러면 다시 우리가 방금전에 만든 HandmadeItr의 반복자를 발생자를 통해서 만들어보겠습니다.
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);
}
참조
https://developer.mozilla.org/ko/docs/Web/JavaScript/Guide/Iterators_and_Generators