06.JavaScript- Iterable Protocol, Iterator Protocol, Generator

이수현·2022년 5월 3일
0

TIL

목록 보기
6/23

📚Iteration protocols

[Iterable protocol, Iterator protocol]

const iterable = {
  [Symbol.iterator]() {
    let i = 3;
    return {
      next() {
        return i == 0 ? { done: true } : { value: i--, done: false };
      },
    };
  },
};
for (const a of iterable) { // iteralbe은 iterable객체이다.
  console.log(a); // for ..of 문을 실행하면 iterable객체의 value를  done값이 true가 되기 전까지 출력한다. 
		 //=> 3 2 1이 한 줄씩 순서대로 출력된다
}
/**
* 위 코드를 보면 객체 안에 [Symbol.iterator]()가 정의되어 있고, 반환값으로 next()
* 메서드가 구현된 객체를 반환하고 있다.
* 
*/

Array, Map, Set, String은 Iterable 객체이다.

기본적으로 위 4가지 타입은 well-formed Iterable 객체라고 한다.
반면에 위의 코드에 있는 변수 iterable은 Non-well-formed Iterable 객체이다.

Array가 어떻게 구현되어있는지 살펴보면, 내부에 IterableIterator를 반환하는[Symbol.iterator]()가 구현되어 있다.
IterableIterator를 보면, 내부에 또 IterableIterator를 반환하는[Symbol.iterator]()가 구현되어 있다.
이것은 다시말하면 let arr = [1,2,3]; let temp = arr[Symbol.iterator](); 이 경우에 temp도 iterable객체가 된다.

그런데 위 개념을 이해하고 코드를 짜보았다.

const arr = [1, 2, 3];
let iter2 = arr[Symbol.iterator]();
let iter3 = iter2[Symbol.iterator]();
let iter4 = iter3[Symbol.iterator]();
console.log(iter4[Symbol.iterator] === iter3);
console.log(iter2.next()); // { value: 1, done: false }
console.log(iter2); // Object [Array Iterator] {}
console.log(iter3); // Object [Array Iterator] {}
console.log(iter4); // Object [Array Iterator] {}

for (const a of iter4) {
  console.log(a); // 2 \n  3
}
// 여기서 당연히 iter2까지는 iterable 객체가 될 것 같다고 예상을 했다.
// 그런데 iter3, iter4까지 iterable 객체가 되는 것을 확인하고 이게 뭐지?? 라고 생각했고 머리가 복잡해졌다.
// 그런데 for ..of 문을 iter4로 순회하는 코드를 짜고, 그 전에 위에서 iter2.next()를 해보았는데,
// iter4의 값이 1 2 3이 아닌 2 3이 나온 것을 확인했다.
// 이것을 확인하고 아마 Array는 내부적으로 [Symbol.iterator]()가 아래와 같이 코드가 되어있을 거라고 알게되었다.
[Symbol.iterator] () {
	return {
    	next() {
        },
      	[Symbol.iterator](){
        	return this;
        }
    }
}
// 자기 자신을 반환하는 [Symbol.iterator]()가 Array의 [Symbol.iterator]() 반환값에 있기 때문에
// next()된 것을 기억하고 2  3이 나온다는 것을 알게되었다. 그리고 이렇게 어디까지 순회를 했는지 기억할 수 있도록 구현되어 있는 것을 well-formed iterable 객체라고 한다.

그렇다면 맨 위에 있는 코드도 well-formed가 되도록 코드를 수정해보자.

const iterable = {
  [Symbol.iterator]() {
    let i = 3;
    return {
      next() {
        return i == 0 ? { done: true } : { value: i--, done: false };
      },
      [Symbol.iterator]() {
      	return this;
      }
    };
  },
};
for (const a of iterable) { // iteralbe은 iterable객체이다.
  console.log(a); 
}

//[Symbol.iterator]() {
//      	return this;
//      }
// 를 추가해줬다.
// 그럼 well-formed iterable 객체가 되었는지 확인을 해보자.
let iterator = iterable[Symbol.iterator]();
let iterator2 = iterator[Symbol.iterator]();
console.log(typeof iterator); // object
console.log(typeof iterator2); // object
iterator2.next();
for (const a of iterator) {
  console.log(a); // iterator2.next()를 했는데 3 2 1이 아닌 2 1이 출력된다. 순회를 기억하는 것을 알 수 있다.
}

Generator

[출처: https://codeburst.io/understanding-generators-in-es6-javascript-with-examples-6728834016d5]

function* makeGenerator2() {
  for (let i = 1; i < 10; i++) {
    yield i * 2;
  }
}
let multiple = makeGenerator2();
// console.log(multiple.next()); // 2
// console.log([...multiple]); // [ 4,  6,  8, 10, 12, 14, 16, 18 ]

for(let a of multiple) {
	console.log(a); // 4 6 8 ... 18
}

위 코드는 Iterable protocol과 Iterator protocol을 준수하기 때문에 generator function의 값이 iterable 객체일 것이라고 예상하고 작성했다.
console.log([...multiple]); // [ 4, 6, 8, 10, 12, 14, 16, 18 ] spread 연산자가 잘 적용되는 것을 보고 iterable 객체라는 것을 확실히 인지하고 for ..of문을 작성하였는데, 역시나 잘 출력되는 것을 확인할 수 있었다.

그리고 객체 내부의 [Symbol.iterator]()를 generator로 변환해서 사용하는 방법도 찾을 수 있었다.

let range = {
  from: 1,
  to: 5,

  [Symbol.iterator]() {
    return {
      current: this.from,
      last: this.to,

      next() {
        if (this.current <= this.last) {
          return { done: false, value: this.current++ };
        } else {
          return { done: true };
        }
      },
      [Symbol.iterator]() {
        return this;
      },
    };
  },
};

위 코드는 일반적인 [Symbol.iterator]()가 선언된 iterable 객체이다. 한 번 변환해보자.

let changeRange = {
  from: 1,
  to: 5,
  *[Symbol.iterator]() { // *[Symbol.iterator]() {} 구문을 사용
    for (let value = this.from; value <= this.to; value++) {
      yield value;
    }
  },
};

let changeRange2 = changeRange[Symbol.iterator]();
let changeRange3 = changeRange2[Symbol.iterator]();
changeRange3.next();
for (let a of changeRange2) {
  log(a); // 2 3 4 5
}

*[Symbol.iterator]() {} 구문을 사용하면 [Symbol.iterator]()이 generator function이 된다.
훨씬 짧게 사용할 수 있다. 그리고 generator function이 되기 때문에 변환하지 않았을 때 작성하던 return 안의 [Symbol.iterator]() {return this;} 를 작성하지 않아도 순회를 기억한다.(=well-formed iterable 객체)

그런데, 공식문서를 읽다보니 yield* 라는 키워드가 존재했다.
무엇을 위해 이런 키워드가 생겼는지 알아보고 예시 코드를 만들어보자.

yield* 공식문서 정의 : yield* 표현식은 다른 generator 또는 iterable 객체에 위임하는 데 사용됩니다.

다른 generator와 iterable 객체에 무엇을 위임할까? 그리고 무엇을 의도해서 위임까지 할 수 있게 만들었을까...
공식문서에 나와있는 예제를 살펴보자.

function* func1() {
  yield 42;
}

function* func2() {
  yield* func1();
}

const iterator = func2();

console.log(iterator.next().value); // 42
//func1() func2() generator function이 2개 선언되어 있고, 
//func2() 내부에 yield* 표현식과 func1() 선언되어 있다.
// 변수 iterator에 func2()를 할당하였고,
// next().value를 출력하니 42가 출력되었다.
// 42는 func1()의 선언된 값이다.
// 이를 통해, 유추해본 것은 generator function은 iterable 객체를 반환하는데, 
// func2()는 같은 iterable 객체인 func1()을 선언하여(func1()에게 위임하여) 
// 42라는 값이 출력되는 결과를 나은 것 같다.
// generator function 1개와 iterable 객체 1개를 만들어서 테스트 해보자.
let changeRange = {
  from: 1,
  to: 5,
  *[Symbol.iterator]() {
    for (let value = this.from; value <= this.to; value++) {
      yield value;
    }
  },
};

function* testGen() {
  yield* changeRange;
}

let testYield = testGen();
log(testYield.next().value); // 1

// iterable 객체인 changeRange를 만들고, generator function인 testGen()에
// yield* changeRange 를 선언하여 changeRange에게 위임을 해보았다.
// 변수 testYield에 testGen()을 선언하고 next().value를 출력해보았더니
// changeRange의 값이 출력되었다.
// 무슨 의도로 이러한 표현식을 만들었는지 아직까지 이해는 하지 못했지만,
// 원리는 이해했으니, 추후에 이런 상황이 나오면 사용해보자!

0개의 댓글