for-of
는 어떻게 동작하는가?[Symbol.iterator]()
는 무엇인가?TL;DR
for-of
는 이터러블 인터페이스인[Symbol.iterator]()
메서드를 호출하고 그 결과 이터레이터가 리턴되어야 정상적으로 동작한다.[Symbol.iterator]()
메서드는 이터러블의 인터페이스로서 이터레이터를 리턴하는 메서드이다.- 이터러블은
[Symbol.iterator]()
메서드를 실행하면 이터레이터를 리턴하는 값을 말한다.- 이터레이터는
next()
메서드를 실행하면{ value, done }
객체를 리턴하는 값을 말한다.- well-formed 이터레이터는
iterator === iterator[Symbol.iterator]()
를 만족하는 이터레이터를 말한다.
const arr = [1, 2, 3];
for (const a of arr) console.log(a); // 1, 2, 3
위 코드를 보면 for...of
가 arr
을 순회하여 요소값을 하나씩 출력하고 있다. 다음 코드를 보자.
const arr = [1, 2, 3];
arr[Symbol.iterator] = null;
// TypeError: arr is not iterable
for (const a of arr) console.log(a);
에러가 발생하면서for-of
가 동작하지 않는다. 에러를 읽어보면 arr
이 iterable
이 아니라고 한다. iterable
은 무엇일까? 단순히 단어 뜻 그대로 순회 가능한 값일까? 그렇다면 자바스크립트 코드에서 정의하는 이터러블의 조건은 무엇일까? 그리고 [Symbol.iterator]
는 무엇일까?
ECMAScript Language Specification - well known symbols에 보면 다음과 같이 나와 있다:
Specification Name | [[Description]] | Value and Purpose |
---|---|---|
@@iterator | Symbol.iterator | A method that returns the default Iterator for an object. Called by the semantics of the for-of statement. |
Symbol.iterator는 객체의 이터레이터를 리턴하는 메서드의 이름이고 for-of
문에 의해 실행된다.
알게 된 내용을 정리해보면 다음과 같다:
for-of
는 객체로부터 Symbol.iterator 메서드를 호출한다for-of
는 객체 타입이 이터러블이 아니라는 TypeError: arr is not iterable
를 발생시킨다따라서:
그리고 Specification Name인 @@iterator를 따라가면 ECMAScript Language Specification - iterable interface가 나온다:
Property | Value | Requirements |
---|---|---|
@@iterator | A function that returns an Iterator object. | A method that returns the default Iterator for an object. |
따라서:
이터레이터의 대한 인터페이스도 마찬가지 문서에서 확인해볼 수 있다. 이터레이터 인터페이스는 next()
메서드를 실행하면 { value, done }
객체를 리턴한다.
지금까지 알게 된 내용을 요약하면 다음과 같다:
for-of
는 이터러블의 인터페이스인 Symbol.iterator 메서드를 실행한다.next()
메서드를 실행하면 { value, done }
객체를 리턴한다.최소한의 로직을 넣어 코드로 구현한 기본 형태는 다음과 같다:
// for-of 순회시 1, 2, 3을 리턴하는 이터러블
const iterable = {
[Symbol.iterator]() {
let i = 1;
return {
next() {
return i === 4
? { done: true }
: { value: i++, done: false };
}
}
}
};
참고로 위 이터러블은 아직 well-formed 이터레이터를 리턴하지 않는데 이에 대해서는 좀 더 뒤에 가서 다룬다.
지금까지 알아본 내용에 의하면 for-of
는 Symbol.iterator 메서드를 실행한다고 했다. 그렇다면 자바스크립트에서 Symbol.iterator 메서드가 내장으로 구현된 값은 무엇일까? Array, Set, Map이 Symbol.iterator 메서드가 구현된 이터러블 값이다.
코드로 보자:
// Array 객체는 Symbol.iterator 메서드가 구현된 이터러블이다
const arr = [1, 2, 3];
// for-of를 통해 arr을 순회할 수 있다
for (const a of arr) console.log(a); // 1, 2, 3
// arr은 이터러블이므로 [Symbol.iterator]() 메서드를 실행하면 이터레이터를 리턴한다
const iterator = arr[Symbol.iterator]();
// 이터레이터의 next() 메서드를 실행하면 { value, done } 객체를 리턴한다
console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 2, done: false }
console.log(iterator.next()); // { value: 3, done: false }
console.log(iterator.next()); // { value: undefined, done: true }
이제 맨 처음에 봤던 코드로 돌아가서 동작하지 않았던 이유를 알아보자:
const arr = [1, 2, 3];
// arr 내부에서 이터레이터를 반환하는 Symbol.iterator를 null로 만든다
arr[Symbol.iterator] = null;
// for-of는 Symbol.iterator 메서드를 실행하는데 null이므로 실행이 불가하다
// 그러므로 arr이 이터러블이 아니라는 에러가 발생한다
// => TypeError: arr is not iterable
for (const a of arr) console.log(a);
이번에는 이터레이터를 꺼내서 직접 next()
메서드를 실행해보자.
const arr = [1, 2, 3];
// arr은 이터러블이므로 [Symbol.iterator]() 메서드를 실행하여 이터레이터를 꺼낸다
const iterator = arr[Symbol.iterator]();
// 이터레이터 next() 메서드를 실행한다
console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 2, done: false }
console.log(iterator.next()); // { value: 3, done: false }
console.log(iterator.next()); // { value: undefined, done: true }
well-formed 이터레이터란 이터레이터이자 이터러블인 값을 말한다. 이게 무슨 말이지? 천천히 살펴보자.
{ value, done }
을 리턴한다그렇다면 이터러블이자 이터레이터라는 것은 다음을 의미한다:
{ value, done }
을 리턴한다이때 이터레이터의 Symbol.iterator 메서드는 이터레이터를 리턴해야 하므로 이미 구현된 이터레이터인 자기 자신을 리턴하면 된다.
코드로 나타내면 다음과 같다:
const iterable = {
[Symbol.iterator]() {
let i = 1;
return {
next() {
return i === 4
? { done: true }
: { value: i++, done: false };
},
[Symbol.iterator() {
return this;
}
}
}
};
이제 iterable은 well-formed 이터레이터를 리턴한다.
자바스크립트 내장 값인 Array에서 꺼낸 이터레이터 또한 well-formed 이터레이터이다. 그래서 이터레이터가 곧 이터러블이므로 이터러블인 arr
이 아니라 arr
에서 꺼낸 이터레이터로 for-of
순회가 가능하다.
const arr = [1, 2, 3];
const iterator = arr[Symbol.iterator]();
for (const a of iterator) console.log(a); // 1, 2, 3
그리고 well-formed 이터레이터의 Symbol.iterator 메서드를 실행하면 자기 자신을 리턴하므로 일치 비교를 하면 참값이 나온다.
console.log(iterator === iterator[Symbol.iterator]()); // true
여기서 생각해볼 점은 well-formed 이터레이터는 자기 자신을 리턴하기 때문에 { value , done }
이 어디까지 진행되었는지 기억할 수 있다는 점이다. 코드로 재현해보자.
const arr = [1, 2, 3];
const iterator = arr[Symbol.iterator]();
// arr의 이터레이터가 방금 꺼낸 iterator를 참조하도록 한다
arr[Symbol.iterator] = () => iterator;
// 이터레이터를 한 번 순회한다
console.log(iterator.next()); // { value: 1, done: false }
// arr을 전체 순회한다
// => 1, 2, 3이 아니라 2, 3을 출력한다
for (const a of arr) console.log(a); // 2, 3
우리가 직접 만든 이터러블로도 재현할 수 있다.
// 순회시 1, 2, 3을 리턴하는 이터러블
const iterable = {
[Symbol.iterator]() {
let i = 1;
return {
next() {
return i === 4
? { done: true }
: { value: i++, done: false };
},
[Symbol.iterator]() {
return this;
}
}
}
};
const iterator = iterable[Symbol.iterator]();
for (const a of iterator) {
console.log(a); // 1
break;
};
for (const a of iterator) {
console.log(a); // 2, 3
};
TIL을 작성하면서 느낀 점을 이야기합니다.
for-of
에 non-well-formed 이터레이터를 넣어서 코드를 실행했다가 에러가 발생했다. 왜 에러가 발생하는지 이해가 안되었다. 그러다 원인을 찾았다(for-of
는 이터러블 인터페이스인 Symbol.iterator 메서드를 실행하는데 non-well-formed 이터레이터는 해당 메서드가 없으므로 TypeError가 발생한다). 이미 문서를 거의 다 작성한 상태였지만 싹 다 갈아엎고 다시 작성했다. 결과적으로 본 포스팅을 작성하는데 2시간이 넘게 걸렸는데, 그래도 덕분에 오늘 작성한 개념에 대해서는 백지장에서부터 바로 설명할 수 있을 것 같다.