1. for ... of
기존 ES5에서와 달리, ES6에서는 for ... of로 배열, Map, Set 등 다양한 데이터 구조를 순회할 수 있다. (가능하다면)
// Array
const arr = [1, 2, 3];
for (const a of arr) console.log(a);
// Set
const aSet = new Set([1, 2, 3]);
for (const a of aSet) console.log(a);
// Map
const map = new Map([
["a", 1],
["b", 2],
["c", 3]
]);
for (const a of map) console.log(a);
단순히 for ... of가 각 데이터를 인덱싱을 통해 순회하는 것 처럼 보일 수 있는데, 인덱싱으로 데이터를 접근해보자.
// Array
const arr = [1, 2, 3];
// 1, 2, 3
console.log(arr[0], arr[1], arr[2]);
// Set
const aset = new Set([1, 2, 3]);
// undefined, undefined, undefined
console.log(aset[0], aset[1], aset[2]);
// Map
const map = new Map([
["a", 1],
["b", 2],
["c", 3]
]);
// undefined, undefined, undefined
console.log(map[0], map[1], map[2]);
배열이야 뭐 인덱싱이 가능해서 잘 나오지만, Set, Map 데이터는 인덱싱이 되지 않는다.
즉, for .. of는 인덱싱이 아닌 다른 방법으로 데이터를 순회한다는 것인데..
2. Iterator
영문 그대로 해석하자면 '반복자'인데,
Iterator는 배열 뿐만 아니라 순회가 가능한 모든 데이터 구조에 대해 순회할 수 있는 함수를 제공하는 값을 의미한다.
말이 좀 어려운데, iterator를 사용해서 알아보자.
const arr = [1, 2, 3];
const arrIterator = arr[Symbol.iterator];
console.log(arrIterator);
브라우저에서 실행해보면,
보다시피 함수다. 실행을 해볼까..
// Array
const arr = [1, 2, 3];
const arrIterator = arr[Symbol.iterator]();
console.log(arrIterator);
next()라는 함수를 가진 Array Iterator이고, Array Iterator의 프로토타입은 object이므로, 객체 정도로 봐도 무방할 것 같다.
next()라는 함수는 벌써부터 냄새가 나지만, 실행은 해보자.
const arr = [1, 2, 3];
const arrIterator = arr[Symbol.iterator]();
// { value: 1, done: false }
console.log(arrIterator.next());
value, done 값을 가진 object가 출력되었는데, value는 arr의 첫번째 값이고, done은 뭔가 끝인지 아닌지 알려주는 값인 것 같다.
코드를 요렇게 바꿔보면,
const arr = [1, 2, 3];
const arrIterator = arr[Symbol.iterator]();
while (!(result = arrIterator.next()).done) {
console.log(result);
}
console.log(arrIterator.next());
이건 다른 Set, Map 등의 Iterable한 데이터 구조도 동일하게 결과를 얻을 수 있다.
즉, Iterator는 { value, done } 객체를 리턴하는 next()를 가진 값으로 생각할 수 있겠고,
Iterable 객체는 Iterator를 리턴하는 [Symbol.iterator]()를 가진 값(object)으로 생각할 수 있겠다. (= Array, Set, Map...)
그리고 for .. of는 이 Iterator/Iterable을 동작하는 하나의 방법이 되겠다.
3. 사용자 정의 Iterable
Iterable에 대해 알아 보았으니, 매우 간단하게 구현이라도 해보자.
우선 Symbol.iterator 키에 대해 next() 함수를 반환해야 하므로,
const iterable = {
[Symbol.iterator]() {
return {
next() {
return { value: undefined, done: false };
}
};
}
};
앞서 보였던 배열 [1, 2, 3]과 같은 동작을 하는 iterable로 변경해보자.
const iterable = {
[Symbol.iterator]() {
let i = 1;
return {
next() {
return i === 4 ? { value: undefined, done: true } : { value: i++, done: false };
}
};
}
};
const iter = iterable[Symbol.iterator]();
for (const a of iter) {
console.log(a);
}
잘 동작한다.
4. well-formed Iterable
사실 우리가 만든 iterable과 다르게 기존 배열, Map, Set은 또다른 특징이 있다.
const arr = [1, 2, 3];
const iter1 = arr[Symbol.iterator]();
const iter2 = iter1[Symbol.iterator]();
const iter3 = iter2[Symbol.iterator]();
for (const a of iter3) {
console.log(a);
}
위 코드를 실행시켜보면, arr을 순회한 결과와 동일하게 출력이 된다.
즉, iterable/iterator 프로토콜을 따르는 데이터 구조들은 iterator를 iterable로 취급하여 다시 iterator를 호출해도 원래 iterable의 iterator를 반환해준다는 의미이다. (?????)
말이 매우 복잡하지만, 간단히 말해 Symbol.iterator 키로 다시 접근하면 기존 객체를 되돌려주면 된다는 의미이다.
코드로 보자면,
const iterable = {
[Symbol.iterator]() {
let i = 1;
return {
next() {
return i === 4 ? { value: undefined, done: true } : { value: i++, done: false };
},
// return this
[Symbol.iterator]() {
return this;
}
};
}
};
const iter1 = iterable[Symbol.iterator]();
const iter2 = iter1[Symbol.iterator]();
const iter3 = iter2[Symbol.iterator]();
for (const a of iter3) {
console.log(a);
}
의도한대로 잘 동작한다.
(+) for ... of 이외에도 ES6의 Spread 연산자(...)도 iterable을 취급할 수 있다.
const arr = [1, 2];
const map = new Map([["a", 1], ["b", 3]]);
arr[Symbol.iterator] = null;
log([...arr, ...map.keys()]);
이 코드를 실행하면, "arr is not iterable"이라는 에러를 확인할 수 있는데,
이를 통해 전개 연산자도 iterable/iterator 프로토콜을 따른다는 것을 알 수 있겠다.
'Common > Javascript' 카테고리의 다른 글
함수형 프로그래밍 - (3) map, filter, reduce (0) | 2021.09.03 |
---|---|
함수형 프로그래밍 - (1) 평가, 일급, 일급 함수, 고차 함수 (0) | 2021.08.26 |