[ 살펴보기 ] Javascript - Iterable, Iterator

[ 살펴보기 ] Javascript - Iterable, Iterator

·

4 min read

Iteration이 가능한 데이터 구조 사양을 정의하기 위해 ECMAScript sepc에 추가된 Iteration protocol 중 Iterable protocol을 준수하는 객체를 Iterable이라고 한다. Iterable 객체는 for..of operator를 통해 loop 작업을 할 수 있고. Spread operator와 array deconstruction을 사용할 수 있는 대상이 된다.

const testArr = ["test", "test address"];

for (const item of testArr) {
  console.log(item);
}

Iterable 객체가 되기 위해선 [System.iterator]() method를 구현하는 객체이거나 prototype chain을 통해 [System.iterator]() method를 상속 받는 객체여야 한다. 위의 예제에서 array를 for..of operator를 통해 loop 작업을 할 수 있는 이유 역시 array가 prototype chain을 통해 System.iterator method를 상속받기 때문이다.

위의 예제와는 반대로 일반 object data는 iterable 객체가 아니므로 for..of operator를 통해 loop 작업이 불가능하다.

const testObj = { name: "test", address: "test address" };

// 일반 object는 iterable 객체가 아니므로 오류 발생
for (const item of testObj) {
  console.log(item);
}

여기서 주의할 점은 위에서 iterable 객체가 spread operator를 사용할 수 있는 대상이 된다고 했지만 일반 object는 iterable 객체가 아님에도 spread operator를 사용할 수 있는 대상이 된다. 즉, object data로 spread operator를 사용할 수 있다. ( object spread 허용 문법 제안은 TC39 stage 4 단계에 있다 - proposal-object-rest-spread )

const testObj = { name: "test", address: "test address" };
const testObj2 = { ...testObj };
// 일반 객체 데이터도 spread 문법이 허용된다.

Buint-in iterable 객체로는 String, Array, Map, Set이 있으며 NodeList와 같은 DOM Collection type 역시 interable 객체다.

const targetClassElements = document.querySelectorAll(".target");
// targetClassElements:NodeList(3)

// NodeList 역시 iterable 객체이므로 for..of으로 loop 작업을 할 수 있다.
for (const item of targetClassElements) {
  console.log(item);
}

Custom iterable

일반 객체일지라도 iterable protocol에 맞게 구현하면 iterable 객체로 사용할 수 있다. 다음 예제를 살펴보자.

const testObj = { name: "test", address: "test address" };

testObj[Symbol.iterator] = function () {
  return {
    targetObj: this,
    currentIndex: 0,
    lastIndex: Object.keys(this).length - 1,
    objectKeys: Object.keys(this),
    next() {
      if (this.currentIndex <= this.lastIndex) {
        const value = this.targetObj[this.objectKeys[this.currentIndex]];
        this.currentIndex++;
        return { done: false, value };
      } else {
        return { done: true };
      }
    },
  };
};

심플한 예제로 일반 객체인 testObj를 iterable 객체로 만들기 위해 Symbol.iterator method를 구현하고 있다.

구현하는 iterator method는 위의 예제와 같이 next method를 반환해야 하고 next method는 IteratorResult interface를 준수하는 객체를 반환해야 한다. IteratorResult 객체는 위의 예제와 같이 done과 value property로 구성된다.

Interation을 수행하며 next method가 done이 false인 IteratorResult를 return하면 아직 iteration이 끝나지 않았음을 뜻하고 done이 true인 IteratorResult를 return하면 iteration이 완료되었음을 뜻한다.

위처럼 custom iterable 객체를 만들었다면 for..of operator를 이용해 loop 작업을 할 수 있다.

for (const item of testObj) {
  console.log({ item });
}

또한 위의 customer iterable object는 이제 array decontruction을 적용할 수 있는 대상이 될 수 있다.

const [frist, ...rest] = testObj;

console.log(frist); // test 
console.log(rest); // ["test address"]

Iterator

Iterable 객체의 Symble.iterator method를 호출하면 " iterator " 객체가 반환된다. 그리고 위의 예제에서 살펴보았듯이 iterator 객체에는 next method를 구현하고 있다. next method를 실행하면 Iterable 객체를 순차적으로 실행하며 현재 순서에 따른 값을 가지고 있는 value property와 Iterable 객체의 loop가 종료 되었는지 여부를 나타내는 done property로 구성된 Iterable result object를 반환한다.

const testArr = ["test", "test address"];

const testArrIterator = testArr[Symbol.iterator]();
// testArr iterable의 iterator 객체

const test1 = testArrIterator.next();
// { "value": "test","done": false }
const test2 = testArrIterator.next();
// { "value": "test address","done": false }
const test3 = testArrIterator.next();
// { "value": undefined, "done": true }

for..of operator는 iterator 객체의 next method를 통해 loop 작업을 진행한다. 각 loop 마다 Iteratle result object의 done이 false면 next method를 통해 다음 loop 작업을 하고 true면 loop를 종료한다.

Custom interable을 구성할 때 iterator의 필수 method인 next외에 return과 throw method도 추가할 수 있다. 이 둘은 optional method이므로 반드시 구현할 필요는 없다.

Return method는 IteratorResult 객체를 반환하며 Return method를 호출한다는 것은 주로 iteration을 더 이상 진행하지 않거나 못한다는 뜻이므로 done은 true로 return한다.

...

testObj[Symbol.iterator] = function () {
  return {
    targetObj: this,
    currentIndex: 0,
    lastIndex: Object.keys(this).length - 1,
    objectKeys: Object.keys(this),
    next() {
      if (this.currentIndex <= this.lastIndex) {
        const value = this.targetObj[this.objectKeys[this.currentIndex]];
        this.currentIndex++;
        return { done: false, value };
      } else {
        return { done: true };
      }
    },
    return() {  // return method
      console.log(" ::: return ::: ");
      return { done: true };
    },
  };
};

for (const item of testObj) {
  throw new Error("test 111");
  console.log(" ::: for .. of ::: ");
  console.log({ item });
}

위와 같이 return method를 custom iterable 객체에 추가하고 해당 객체를 통해 loop 작업을 하는 for..of 구문에서 error를 throw하면 위에서 구현했던 return method가 호출되는 것을 확인할 수 있다.