이 글은 유인동님의 함수형 프로그래밍 강의내용을 정리한 글입니다.
기존방식 (es6이전) : for i++
// length와 인덱스에 의존
const list = [1, 2, 3];
for (var i = 0; i < list.length; i++) {
log(list[i]);
}
// 유사배열도 length와 인덱스에 의존
const str = 'abc';
for (var i = 0; i < str.length; i++) {
log(str[i]);
}
ES6 : for of
for (const a of list) {
log(a);
}
for (const a of str) {
log(a);
}
for of
문을 활용하여 인덱스와 length
에 의존하지 않고 간결하게 리스트의 요소들을 하나씩 가지고 올 수 있다.
그렇다면 for of
순회는 어떻게 동작할까?
for of
문은 유사배열 타입(문자열, Set, Map)에 모두 사용이 가능하다.
const arr = [1, 2, 3];
for (const a of arr) log(a);
배열은 인덱싱이 가능하다. 그렇다면 for of
문은 length
와 인덱싱을 통해 동작하는 것일까?
우선 아래 내용을 더 확인해보자.
const set = new Set([1, 2, 3]);
for (const a of set) log(a);
Set은 인덱싱이 불가능하고 키로 조회가 가능하지만 순회가 가능하다.
const map = new Map([['a', 1], ['b', 2], ['c', 3]]);
for (const a of map) log(a);
Map도 인덱싱이 불가능하고 키로 조회가 가능하지만 순회가 가능하다.
따라서
for of
문이 내부적으로length
와 인덱싱을 통해 동작하는게 아니라는 것을 알 수 있다.
그렇다면 어떻게 for of
문이 동작할까?
심볼은 ES6부터 도입된 새로운 타입으로 객체의 키(인덱스)로 사용할 수 있다.
한번 배열에 Symbol.iterator
로 인덱싱 해보자.
const arr = [1, 2, 3];
log(arr[Symbol.iterator]); // 어떤 함수가 출력된다.
arr[Symbol.iterator] = null; // 삭제해보기
for (const a of arr) log(a); // 에러 : arr가 iterable이 아니다.
arr뿐만 아니라 set, map에서도 똑같이 Symbol.iterator
가 존재한다.
이를 보아 for of
문과 Symbol.iterator
과 서로 연관이 있을것이다 라는것을 알 수 있다.
배열, Set, Map은 자바스크립트의 내장객체로써 이터러블 / 이터레이터 프로토콜을 따른다.
이터레이터를 리턴한는
[Symbol.iterator]()
라는 메서드를 가진 값
배열, Set, Map 3개의 객체는 이터러블인데 그 이유는 [Symbol.iterator]()
라는 메서드를 가지고 있기 때문이다. 객체[Symbol.iterator]
로 확인해보면 메서드를 가지고 있는것을 확인할 수 있다.
그리고 객체[Symbol.iterator]()
로 메서드를 실행하면 이터레이터를 리턴한다.
const arr = [1, 2, 3];
arr[Symbol.iterator]; // ƒ values() { [native code] }, 메서드
arr[Symbol.iterator](); // Array Iterator {} <- 배열형 이터레이터 객체
{ value, done}
객체를 리턴하는next()
라는 메서드를 가진 값
const arr = [1, 2, 3];
let iterator = arr[Symbol.iterator]();
iterator.next(); // {value: 1, done: false}
iterator.next(); // {value: 2, done: false}
iterator.next(); // {value: 3, done: false}
iterator.next(); // {value: undefined, done: true}
이터러블의 메서드를 통해서 이터레이터를 리턴받을수 있고 이터레이터의 next()
메서드를 통해서 이터러블 객체들의 순회정보를 확인할 수 있다.
이때 for of
문은 이터레이터의 next()
를 호출하면서 {value, done}
객체를 받아오고 value의 값을 사용한다. 그러다 done
이 false이면 순회를 종료한다.
const arr = [1, 2, 3];
let iterator = arr[Symbol.iterator]();
iterator.next();
for (const a of iterator) log(a);
// 2
// 3
이터레이터를 통해서 순회가 가능하고 이터레이터를 일부 진행시키고 순회하면 진행된 곳부터 시작된다.
이터러블 객체를
for of
와 전개 연산자 등과 함께 동작하도록 규약
정리하면 배열, Set, Map은 이터러블 객체이고 이터레이터를 리턴할 수 있도록 규약을 따르고 있기 때문에 for of
문과 여러가지 이터러블 연산자들을 사용할 수 있다.
이터레이터 프로토콜은 배열, Set, Map 뿐만아니라 순회가 가능한 형태는 이터러블 프로토콜을 따른다.
또한 다양한 오픈소스와 자바스크립트가 사용되는 환경인 브라우저의 웹API들, DOM 등 들에도 이터러블 프로토콜을 따르고 있다.
const all = document.querySelectorAll('*');
let iterator = all[Symbol.iterator]();
log(iterator);
이터러블 객체의 이터레이터를 반환하는 메서드
추가로 map객체.keys()의 리턴값도 이터레이터 이고 이 이터레이터의 next()
를 찍어보면 맵의 키를 밸류로 가지고 있는 {value, done} 객체를 리턴한다.
따라서 아래 코드와 같이 key들을 순회할 수 있다.
const map = new Map([['a', 1], ['b', 2], ['c', 3]]);
for (const a of map.keys()) log(a);
// a
// b
// c
key()
메서드와 비슷하게 values()
, entries()
메서드들도 같은 방식으로 구현되어 동작된다.
새로운 이터러블 객체 만들어 보면서 이터러블에 대해 깊게 알아보자.
[Symbol.iterator]()
메서드 구현하기return next()
// 사용자 정의 이터러블 //
const iterable = {
[Symbol.iterator]() {
let i = 3;
return {
next() {
return i == 0 ? {done: true} : {value: i--, done: false};
},
[Symbol.iterator]() {
return this; // *(1)
}
}
}
};
// 테스트 //
for (const a of iterable) log(a);
// 3
// 2
// 1
let iterator = iterable[Symbol.iterator]();
iterator.next();
for (const a of iterator) log(a);
// 2
// 1
*(1)
의 this는
{
next() {
return i == 0 ? {done: true} : {value: i--, done: false};
},
[Symbol.iterator]() {
return this; // *(1)
}
}
이다.
이터레이터의 이터레이터
이터레이터의 이터레이터를 자기 자신(이터레이터)으로 반환되도록 해야 잘 만들어진 이터러블이라고 할 수 있다.
이터러블은 이터레이터로도 순회가 가능해야하기 때문이고 이미 진행된 자기자신의 상태를 다시 순회할 수 있어야하기 때문이다.
즉, 이터러블의 이터레이터도 이터러블해야한다.
전개연산자란?
이터러블 객체의 요소를 전개해준다. ...이터러블객체
키워드로 사용한다.
const a = [1, 2, 3, 4];
log(...a);
// 1, 2, 3, 4
전개연산자가 이터러블 프로토콜을 따르는지 확인하기
const a = [1, 2, 3, 4];
a[Symbol.iterator] = null;
log(...a);
// 에러 : not iterable
이터러블 객체가 아니라면 전개연산자를 사용할 수 없다.