[ES6] Iterator

ES6에서 Symbol이 나오면서 도입된 이터레이션 프로토콜은, 데이터 컬렉션을 순회하기 위한 프로토콜이다. 이터레이션 프로토콜을 준수한 객체는 for..of 문으로 순회가 가능하고, Spread 문법의 피연산자가 될 수 있다. 이터레이션 프로토콜을 준수한 객체를 Iterable이라고 한다. Iterable은 내부적으로 Symbol.iterator 메소드를 가지고 있는 객체이다. 예를 들면 배열이 바로 Iterable이다.

const array = [1, 2, 3];

// 배열은 Symbol.iterator 메소드를 소유한다.
// 따라서 배열은 이터러블 프로토콜을 준수한 이터러블이다.
console.log(Symbol.iterator in array); // true

// 이터러블 프로토콜을 준수한 배열은 for...of 문에서 순회 가능하다.
for (const item of array) {
  console.log(item);
}

const obj = { a: 1, b: 2 };

// 일반 객체는 Symbol.iterator 메소드를 소유하지 않는다.
// 따라서 일반 객체는 이터러블 프로토콜을 준수한 이터러블이 아니다.
console.log(Symbol.iterator in obj); // false

// 이터러블이 아닌 일반 객체는 for...of 문에서 순회할 수 없다.
// TypeError: obj is not iterable
for (const p of obj) {
  console.log(p);
}

Symbol.iterator가 객체에 특수 내장 메소드로 존재하지 않으면, for...of가 실행되지 않는다. 즉, for...of가 시작되자마자 Symbol.iterator를 호출한다. 또한 Symbol.iterator는 이터레이터를 반환해야한다. 여기서 이터레이터란 내부적으로 next() 메소드를 가지고 있는 애들이다. 그리고 for...of로 동작하는 아이들은 모두 이터레이터들이다.

for...of는 이터레이터의 next() 메서드를 호출한다. 그리고 next()의 반환값은 {done: Boolean, value: any}와 같은 형태이다. done=true인 경우, 반복이 종료되었음을 의미한다. 반대로는 value 값에 다음 값이 저장된다.

정리하면, 배열, 문자열은 대표적인 이터러블이다. 이터러블은 리스트를 일반화시킨 객체이다. for...of가 시작되면, 다음 값이 필요할 때, next() 메소드를 이용하여 돌아간다. 여기서 next()의 반환값은 키밸류 형태의 객체여야 한다. 정확히는 {done: Boolean, value: any} 이런 형태가 이루어지는데, 여기서 key값이 done=false 일때는 value에 다음 값(any)이 저장되도록 한다. (순회가 계속 되도록 한다.)

Iterable와 Array-like

ES6부터 이터러블이라는 것이 나오면서 확실히 순회가능한 종류들을 Symbol.iterator 메소드로 구분하지만, 사실 ES5 까지 계속 사용됐었던 유사 배열과 아주 비슷해보인다. 하지만 이 둘은 다르다.

  • 이터러블은 Symbol.iterator 메소드가 존재하는 객체이다.
  • Array-likeindex, length 프로퍼티만 존재하여 배열처럼 보이는 객체이다.

유사배열, 그리고 이터러블 기능을 모두 가지고 있는 객체도 존재한다. 이 말은 for...of를 사용할 수 있고, index, length 프로퍼티 또한 가지고 있는 객체인데, 이는 문자열이 대표적인 예이다.

Array-like !== Iterable

Array.from

여기서 주의할 것은, 이터러블과 유사배열 객체는 Array가 아니라서, push, pop 등의 메소드를 지원하지 않는다. 이럴 때 사용하는 Array 메소드가 Array.from!!

Array.from은 범용 메소드로 이터러블, 유사배열 객체 둘다 파라미터로 받아서 리얼 Array로 만들어준다. 이렇게 한번 변환시켜주고 나서 배열 메소드를 사용할 수 있게된다.

let arrayLike = {
  0: "Hello",
  1: "World",
  length: 2
};

let arr = Array.from(arrayLike); // (*)
alert(arr.pop()); // World (메서드가 제대로 동작합니다.)

Iterator

이터러블 프로토콜에는 IterableIterator 두 가지 형태가 존재한다. 우선 Iterable은 위에서 언급했듯이 객체 내부를 순회할 수 있는 객체라는 의미이다. 객체 내부적으로 Symbol.iterator를 확인하여 for...of를 이용해서 순회한다.

const iterable = new Object();

iterable[Symbol.iterator] = function* () {
  yield 1;
  yield 2;
  yield 3;
};

console.log([...iterable]); // 1 2 3
for(var value of iterable) {
    console.log(value); // 1 2 3
}

IterableIterator의 정확한 구분을 하자면, Iterable 객체는 내부적으로 @@iterator 프로퍼티를 포함하고 있다. 그리고 이 프로퍼티가 Iterator를 말하는 것으로, 이 프로퍼티는 Iterator 객체를 반환한다.. 그리고 Iterator라는 인터페이스는 내부적으로 next라는 프로퍼티를 포함하고 있다. next라는 아이는 IteratorResult라는 객체를 반환하는 함수이다.

next()라는 프로퍼티는 정확히 IteratorResult 인터페이스를 따르는 객체를 리턴한다. 만약 이전 next 메소드가 호출됐다면 IteratorResult 객체가 반환되는데, 그 값이 위에서 말한 {done: Boolean, value: any} 이런 식으로 되어있는 객체이다. done=true 일때까지 순회한다. next() 메소드를 가지고 있고, 해당 역할까지 수행하는 인터페이스가 Iterator으로, Iterable만으로는 확연히 차이가 있다고 말할 수 있다.

그리고 결국, 이 Iterator를 가지고 있는 인터페이스가 Iterable이 된다. 다시말하면, [[Iterator]]를 가지고 있는 객체일 것이다.

참고