반복 가능한(iterable, 이터러블) 객체는 배열을 일반화한 객체이다.
배열을 일반화한 객체?
배열과 비슷한 형태와 기능을 가지면서도 내부 구현이 다른 객체.
= 반복 가능한 객체(iterable objects)
→ Array, Map, Set 등은 물론이며 String도 반복 가능
이터러블이라는 개념을 사용하면 어떤 객체에든 for..of 반복문을 적용할 수 있다.
반대로 이터러블이 아닌 객체는 for...of을 적용할 수 없다.
하지만 이러한 객체가 어떤 것들의 컬렉션(목록, 집합 등)을 나타내고 있는 경우, for..of 반복문을 사용할 수 있다면 컬렉션을 순회하는데 유용할 것이다.
이것을 가능하게 해보자!
for..of를 적용하기에 적합해 보이는 배열이 아닌 객체를 만들고 이 객체를 이터러블로 만들어보자!
예시의 객체 range는 숫자 간격을 나타낸다.
let range = {
from: 1,
to: 5
};
// 아래와 같이 for..of가 동작할 수 있도록 하는 게 목표입니다.
// for(let num of range) ... num=1,2,3,4,5
Symbol.iterator(특수 내장 심볼)이라는 메서드를 객체에 추가하면
배열이 아닌 객체에 for...of을 사용할 수 있게 된다.
for..of가 시작되자마자 for..of는 Symbol.iterator를 호출한다.(Symbol.iterator가 없으면 에러).
Symbol.iterator는 반드시 이터레이터(interator)를 반환해야 한다.
이터레이터
: 이터러블 객체를 탐색하는 방법을 정의한 객체. 메서드 next()를 가진다.next()
: 호출 시 이터레이터 내부의 다음 값에 대한 두 프로퍼티 value, done을 반환한다.
{done: Boolean, value: any}의 형태value
: 이터레이터에서 반환된 값done
: 이터레이터 내부의 모든 값을 반환했는지를 나타내는 boolean 값
done=true는 반복이 종료되었음을 의미한다.
done=false일땐 value에 다음 값이 저장된다.
이후 for..of는 반환된 객체(이터레이터)만을 대상으로 동작한다.
for..of에 다음 값이 필요하면, for..of는 이터레이터의 next()를 호출한다.
range를 반복 가능한 객체로 만들어주는 코드
let range = { from: 1, to: 5 }; // 1. for..of 최초 호출 시, Symbol.iterator가 호출됩니다. range[Symbol.iterator] = function() { // Symbol.iterator는 이터레이터 객체를 반환합니다. // 2. 이후 for..of는 반환된 이터레이터 객체만을 대상으로 동작하는데, 이때 다음 값도 정해집니다. return { current: this.from, last: this.to, // 3. for..of 반복문에 의해 반복마다 next()가 호출됩니다. next() { // 4. next()는 값을 객체 {done:.., value :...}형태로 반환해야 합니다. if (this.current <= this.last) { return { done: false, value: this.current++ }; } else { return { done: true }; } } }; }; // 이제 의도한 대로 동작합니다! for (let num of range) { alert(num); // 1, then 2, 3, 4, 5 }
이터러블 객체의 핵심은 '관심사의 분리(Separation of concern, SoC)'에 있다.
이렇게 하면 이터레이터 객체와 반복 대상인 객체(range)를 분리할 수 있다.
이터레이터 객체와 반복 대상 객체를 합쳐서 range 자체를 이터레이터로 만들면 코드가 더 간단해진다.
let range = {
from: 1,
to: 5,
[Symbol.iterator]() {
this.current = this.from;
return this;
},
next() {
if (this.current <= this.to) {
return { done: false, value: this.current++ };
} else {
return { done: true };
}
}
};
for (let num of range) {
alert(num); // 1, then 2, 3, 4, 5
}
이제 range[Symbol.iterator]()가 객체 range 자체를 반환(return this)하여 range가 이터러블 객체가 되었다.
for...of는 반환된 이터러블 객체(여기선 range)의 next()를 실행하기 때문에 range에 정의된 next()를 실행할 수 있게 된다.
this.current에 반복이 얼마나 진행되었는지를 나타내는 값도 저장되어 있다.
코드는 더 짧아졌다. 이렇게 작성하는 게 좋을 때가 종종 있다.
단점은 두 개의 for..of 반복문을 하나의 객체에 동시에 사용할 수 없다는 점이다. 이터레이터(객체 자신)가 하나뿐이어서 두 반복문이 반복 상태를 공유하기 때문이다.
그런데 동시에 두 개의 for..of를 사용하는 것은 비동기 처리에서도 흔한 케이스는 아니다.
배열과 문자열은 가장 광범위하게 쓰이는 내장 이터러블이다.
for..of는 문자열의 각 글자를 순회합니다.
for (let char of "test") {
// 글자 하나당 한 번 실행됩니다(4회 호출).
alert( char ); // t, e, s, t가 차례대로 출력됨
}
서로게이트 쌍(surrogate pair)에도 잘 동작한다.
let str = '𝒳😂';
for (let char of str) {
alert( char ); // 𝒳와 😂가 차례대로 출력됨
}
for...of 없이 이터레이터를 직접 호출하여 문자열 순회하기
문자열 이터레이터를 만들고, 여기서 값을 ‘수동으로’ 가져오는 예시
let str = "Hello"; // for..of를 사용한 것과 동일한 작업을 합니다. // for (let char of str) alert(char); let iterator = str[Symbol.iterator](); while (true) { let result = iterator.next(); if (result.done) break;//이터레이터가 모든 값을 순회했으므로 > break alert(result.value); // 글자가 하나씩 출력됩니다. }
이터레이터를 명시적으로 호출하는 경우는 거의 없는데, 이 방법을 사용하면 for..of를 사용하는 것보다 반복 과정을 더 잘 통제할 수 있다는 장점이 있다.
반복을 시작했다가 잠시 멈춰 다른 작업을 하다가 다시 반복을 시작하는 것과 같이 반복 과정을 여러 개로 쪼개는 것도 가능하다.
비슷해 보이지만 아주 다른 용어 두 가지
브라우저 등의 호스트 환경에서 자바스크립트를 사용해 문제를 해결할 때 이터러블 객체나 유사 배열 객체 혹은 둘 다인 객체를 만날 수 있다.
문자열이 둘 다인 객체의 대표적인 예이다.
이터러블 객체라고 해서 유사 배열 객체는 아니다.
유사 배열 객체라고 해서 이터러블 객체인 것도 아니다.
위 예시의 range는 이터러블 객체이긴 하지만 인덱스도 없고 length 프로퍼티도 없으므로 유사 배열 객체가 아니다.
유사 배열 객체이긴 하지만 이터러블 객체가 아닌 예시
let arrayLike = { // 인덱스와 length프로퍼티가 있음 => 유사 배열 0: "Hello", 1: "World", length: 2 }; // Symbol.iterator가 없으므로 에러 발생 ## for (let item of arrayLike) {}
이터러블과 유사 배열은 대개 배열이 아니기 때문에 push, pop 등의 메서드를 지원하지 않는다.
범용 메서드 Array.from는 이터러블이나 유사 배열을 받아 ‘진짜’ Array를 만들어 배열 메서드를 사용할 수 있게 만들어 준다.
예시:
let arrayLike = { 0: "Hello", 1: "World", length: 2 }; let arr = Array.from(arrayLike); alert(arr.pop()); // World (메서드가 제대로 동작합니다.)
Array.from 호출 시 객체를 받아 이터러블이나 유사 배열이 맞으면 새로운 배열을 만들고 객체의 모든 요소를 새롭게 만든 배열로 복사한다.
이터러블을 사용한 예시
// range는 챕터 위쪽 예시에서 그대로 가져왔다고 가정합시다. let arr = Array.from(range); alert(arr); // 1,2,3,4,5 (배열-문자열 형 변환이 제대로 동작합니다.)
Array.from엔 ‘매핑(mapping)’ 함수를 선택적으로 넘겨줄 수 있다.
Array.from(obj[, mapFn, thisArg])
mapFn을 두 번째 인수로 넘겨주면 새로운 배열에 obj의 요소를 추가하기 전에 각 요소를 대상으로 mapFn을 적용할 수 있다. 새로운 배열엔 mapFn을 적용하고 반환된 값이 추가된다.
세 번째 인수 thisArg는 각 요소의 this를 지정할 수 있도록 해준다.
예시:
// range는 챕터 위쪽 예시에서 그대로 가져왔다고 가정합시다. // 각 숫자를 제곱 let arr = Array.from(range, num => num * num); alert(arr); // 1,4,9,16,25 아래 예시에선 Array.from를 사용해 문자열을 배열로 만들어보았습니다. let str = '𝒳😂'; // str를 분해해 글자가 담긴 배열로 만듦 let chars = Array.from(str); alert(chars[0]); // 𝒳 alert(chars[1]); // 😂 alert(chars.length); // 2
Array.from은 str.split과 달리, 문자열 자체가 가진 이터러블 속성을 이용해 동작하기 때문에 for..of처럼 서로게이트 쌍에도 제대로 적용됩니다.
위 예시는 기술적으로 아래 예시와 동일하다.
let str = '𝒳😂'; let chars = []; // Array.from 내부에선 아래와 동일한 반복문이 돌아갑니다. for (let char of str) { chars.push(char); } alert(chars);
어쨌든 Array.from을 사용한 예시가 더 짧습니다.
Array.from을 사용하면 서로게이트 쌍을 처리할 수 있는 slice를 직접 구현할 수도 있다.
function slice(str, start, end) {
return Array.from(str).slice(start, end).join('');
}
let str = '𝒳😂𩷶';
alert( slice(str, 1, 3) ); // 😂𩷶
// 내장 순수 메서드는 서로게이트 쌍을 지원하지 않습니다.
alert( str.slice(1, 3) ); // 쓰레깃값 출력 (영역이 다른 특수 값)