https://helloworldjavascript.net/pages/260-iteration.html를 참조하여 쓴 글입니다.

Iterable

iterable obect는 for…of와 함께 ES2015에서 도입되었습니다.

반복 가능한 객체를 다른 객체와 구분짓는 특징은, 객체의 Symbol.iterator 속성에 특별한 형태의 함수가 들어있다는 것입니다.

const str = 'hello';
str[Symbol.iterator]; // [Function]

객체의 Symbol.iterator 속성에 특정 형태의 함수가 들어있다면, 이를 반복 가능한 객체(iterable object) 혹은 줄여서 iterable이라 부르고, 해당 객체는 iterable protocol을 만족한다고 말합니다. 이런 객체들에 대해서는 ES2015에서 추가된 다양한 기능들을 사용할 수 있습니다.

내장된 생성자 중 iterable 객체를 만들어내는 생성자에는 아래와 같은 것들이 있습니다.

  • String
  • Array
  • TypedArray
  • Map
  • Set

Iterable의 사용

어떤 객체가 Iterable이라면, 그 객체에 대해서 아래의 기능들을 사용할 수 있습니다.

  • for...of 루프
  • spread 연산자 (...)
  • 분해대입(destructuring assignment)
  • 기타 iterable을 인수로 받는 함수

즉, 문자열에 대해서도 위 기능들을 사용할 수 있습니다. 아래의 코드를 실행하고 그 결과를 직접 확인해보세요.

// `for...of`
for (let c of 'hello') {
  console.log(c);
}

// spread 연산자
const characters = [...'hello'];

// 분해대입
const [c1, c2] = 'hello';

// `Array.from`은 iterable 혹은 array-like 객체를 인수로 받습니다.
Array.from('hello');

Generator 함수

Iterable을 구현하는 가장 쉬운 방법은 ES2015에 도입된 generator 함수를 사용하는 것입니다.

Generator 함수는 iterable 객체를 반환하는 특별한 형태의 함수입니다.

// 함수 선언
function* gen1() {}
// 함수 표현식
const gen2 = function* () {}
// 메서드 문법
const obj = {
	* gen3() {}
}

Generator 함수를 호출하면 객체가 생성되는데, 이 객체는 iterable protocol을 만족합니다. 즉, Symbol.iterator 속성을 갖고 있습니다.

function* gen1() {}
const iterable = gen1()
iterable[Symbol.iterator] // [Function]

Generator 함수 안에서는 yield라는 특별한 키워드를 사용할 수 있습니다. Generator 함수 안에서 yield 키워드는 return과 유사한 역할을 하며, iterable의 기능을 사용할 때 yield 키워드 뒤에 있는 값들을 순서대로 넘겨줍니다.

function* numberGen() {
  yield 1;
  yield 2;
  yield 3;
}

// 1, 2, 3이 순서대로 출력됩니다.
for (let n of numberGen()) {
  console.log(n);
}

yield* 표현식을 사용하면, 다른 generator 함수에서 넘겨준 값을 대신 넘겨줄 수도 있습니다.

function* numberGen() {
  yield 1;
  yield 2;
  yield 3;
}

function* numberGen2() {
  yield* numberGen();
  yield* numberGen();
}

// 1, 2, 3, 1, 2, 3이 순서대로 출력됩니다.
for (let n of numberGen2()) {
  console.log(n);
}

Generator 함수를 사용할 때 주의할 점이 있습니다.

  • Generator 함수로부터 생성된 iterable은 한 번만 사용될 수 있습니다.
  • Generator 함수 내부에서 정의된 일반 함수에서는 yield 키워드를 사용할 수 없습니다.
// Generator 함수로부터 생성된 iterable은 한 번만 사용될 수 있습니다.
function* gen() {
  yield 1;
  yield 2;
  yield 3;
}

const iter = gen();

for (let n of iter) {
  // 잘 출력됩니다.
  console.log(n);
}
for (let n of iter) {
  // `iter`는 한 번 사용되었으므로, 이 코드는 실행되지 않습니다.
  console.log(n);
}
// Generator 함수 내부에서 정의된 일반 함수에서는 `yield` 키워드를 사용할 수 없습니다.
function* gen2() {
  // 아예 문법 오류가 납니다. (Unexpected token)
  function fakeGen() {
    yield 1;
    yield 2;
    yield 3;
  }
  fakeGen();
}

Iterator Protocol

Iterable과 Iterator를 잘 구분해야 합니다.

iterable 객체는 iterable protocol을 만좁한다. 즉, Symbol.Iterator 속성에 특별한 형태의 함수가 저장되어 있다.

Iterable Protocol을 만족하려면, Symbol.iterator 속성에 저장되어 있는 함수는 iterator 객체를 반환해야 합니다.

iterator 객체는 아래의 특별한 조건을 만족하는 객체입니다.

  • iterator는 next라는 메서드를 갖습니다.
  • next 메서드는 다음 두 속성을 갖는 객체를 반환해야 합니다.

done - 반복이 모두 끝났는지를 나타냅니다.

value - 현재 순서의 값을 나타냅니다.

위 조건을 iterator protocol 이라고 합니다.

const strIterator = 'go'[Symbol.iterator]();
strIterator.next(); // { value: 'g', done: false }
strIterator.next(); // { value: 'o', done: false }
strIterator.next(); // { value: undefined, done: true }
strIterator.next(); // { value: undefined, done: true }

function* gen() {
	yield 1;
	yield 2;
}

const genIterator = gen()[Symbol.iterator]();
genIterator.next(); // { value: 1, done: false }
genIterator.next(); // { value: 2, done: false }
genIterator.next(); // { value: undefined, done: true }
genIterator.next(); // { value: undefined, done: true }

iterable을 만들어 보기.

function range(start = 0, end = Infinity, step = 1) {
	return {
		currentValue: start,
		[Symbol.iterator]() {
			return {
				next: () => {
					if (this.currentValue < end) {
						const value = this.currentValue;
						this.currentValue += step;
						return {
							done: false,
							value
						}
					} else {
						return { done: true }
					}
				}
			}
		}
	}
}

Generator와 Iterator

Generator 함수로부터 만들어진 객체는 일반적인 iterable처럼 쓸 수 있지만, iterator와 관련된 특별한 성질을 갖고 있습니다.

generator 함수로부터 만들어진 객체는 iterable protocol과 iterator protocol을 동시에 만족합니다.

function* gen() {
	// ...
}

const genObj = gen();
genObj[Symbol.iterator]().next === genObj.next; // true

Symbol.iterator를 통해 iterator를 생성하지 않고도 바로 next를 호출 할 수 있습니다.

function* gen() {
  yield 1;
  yield 2;
  yield 3;
}

const iter = gen();

iter.next(); // { value: 1, done: false }
iter.next(); // { value: 2, done: false }
iter.next(); // { value: 3, done: false }
iter.next(); // { value: undefined, done: true }

generator 함수 안에서 return 키워드를 사용하면 반복이 바로 끝나면서 next 메서드에서 반환되는 객체의 속성에 앞의 반환값이 저장됩니다. 다만, return을 통해 반환된 값이 반복 절차에 포함되지는 않습니다.

function* gen() {
  yield 1;
  return 2; // generator 함수는 여기서 종료됩니다.
  yield 3;
}

const iter = gen();

iter.next(); // { value: 1, done: false }
iter.next(); // { value: 2, done: true }
iter.next(); // { value: undefined, done: true }

// `1`만 출력됩니다.
for (let v of gen()) {
  console.log(v);
}

generator 함수로부터 생성된 객체의 next 메서드에 인수를 주어서 호출하면, generator 함수가 멈췄던 부분의 yield 표현식의 결과값은 앞에서 받은 인수가 됩니다.

function* gen() {
  const received = yield 1;
  console.log(received);
}

const iter = gen();
iter.next(); // { value: 1, done: false }

// 'hello'가 출력됩니다.
iter.next('hello'); // { value: undefined, done: true }

Generator Examples

// 각 항목을 변환한 후 넘겨주기
function* map(iterable, mapper) {
  for (let item of iterable) {
    yield mapper(item);
  }
}

// 각 순서까지의 누적값을 넘겨주기
function* reduce(iterable, reducer, initial) {
  let acc = initial;
  for (let item of iterable) {
    acc = reducer(acc, item);
    yield acc;
  }
}

// 조건에 만족하는 항목만 넘겨주기
function* filter(iterable, predicate) {
  for (let item of iterable) {
    if (predicate(item)) {
      yield item;
    }
  }
}

// 여러 iterable을 연결하기
function* concat(iterables) {
  for (let iterable of iterables) {
    yield* iterable;
  }
}

// 앞쪽 몇 개의 항목만 넘겨주기
function* take(iterable, count = Infinity) {
  const iterator = iterable[Symbol.iterator]();
  for (let i = 0; i < count; i++) {
    // `yield*`와는 다르게, iterator의 `next` 메소드를 이용하면 iterable의 일부만 가져올 수 있습니다.
    const {value, done} = iterator.next();
    if (done) break;
    yield value;
  }
}
← Go home