iterable 객체 파트 링크 : https://ko.javascript.info/iterable
반복 가능한(iterable, 이터러블) 객체는 배열을 일반화한 객체로 이터러블 이라는 개념을 사용하면 어떤 객체에든 for~of 반복문을 적용할 수 있다.
*일반화 : 특수한 개념으로부터 공통된 개념을 묶는 것이다. 예를들어 사람, 돼지, 원숭이는 포유류로 일반화 될 수 있다.
일반화의 개념으로 짐작해볼 때 이터러블 객체는 배열을 객체로 일반화한 객체인 모양이다.
배열은 대표적인 이터러블이다(map 함수로 주로 반복문을 사용한다). 배열 외에도 다수의 내장 객체가 반복 가능하고 문자열 역시 이터러블 중 하나다.
배열이 아닌 객체가 있는데, 이 객체가 어떤 것들의 컬렉션(목록, 집합 등)을 나타내고 있는 경우, for~of 문법을 적용 가능하도록 하여 컬렉션 순회가 가능하도록 해보자. (기존에 단순 객체에서는 for~in 문법을 적용했었다.)
for~of를 적용하기에 적합해 보이는 배열이 아닌 객체(이터러블)를 만들어보자.
예시의 객체 range는 숫자 간격을 나타낸다.
let range = {
from: 1,
to: 5
};
// 아래와 같이 for~of가 동작할 수 있도록 하는 게 목표입니다.
// for(let num of range) ... num=1,2,3,4,5
range를 이터러블로 만들려면(for~of가 동작하도록 하려면) 객체에 Symbol.iterator(특수 내장 심볼)라는 메서드를 추가해야합니다.
Symbol.iterator 메서드를 추가하면?
for~of가 시작되자마자
1) for~of는 Symbol.iterator를 호출(Symbol.iterator가 없으면 에러가 발생합니다). Symbol.iterator는 반드시 이터레이터(iterator, next 메서드가 있는 객체)를 반환해야 한다.
2) 이후 for~of는 반환된 객체(이터레이터)만을 대상으로 동작한다.
for~of에 다음 값이 필요하면,
3) for~of는 이터레이터의 next()메서드를 호출합니다.
next()의 반환 값은 {done: Boolean, value: any}와 같은 형태여야 한다.
done=true는 반복이 종료되었음을 의미하고, done=false일땐 value에 다음 순회 값이 저장된다.
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 };
}
}
};
};
//from이 to보다 작으면 from을 1씩 증가시키며(=next()의 내용) 반복 도는 것
// 이제 의도한 대로 동작한다!!
for (let num of range) {
alert(num); // 1, 2, 3, 4, 5
}
이터러블 객체의 핵심은 '관심사의 분리(Separation of concern, SoC)'에 있다.
range는 이터레이터 객체가 아니기 때문에 메서드 next()가 없다.
대신 rangeSymbol.iterator를 호출해서 만든 ‘이터레이터’ 객체와 이 객체의 메서드 next()에서 반복에 사용될 값을 만들어낸다(예시의 from과 to).
이렇게 하면 이터레이터 객체(예시의 current, last를 담은 객체)와 반복 대상인 객체(range)를 분리할 수 있다.
이터레이터 객체와 반복 대상 객체를 합쳐서 range 자체를 이터레이터로 만들면 코드가 더 간단해진다.
let range = {
//range 객체 값
from: 1,
to: 5,
//range의 특수 내장심볼 Symbol.iterator 호출
[Symbol.iterator]() {
this.current = this.from;
return this; // current라는 프로퍼티를 range 객체에 추가하는 셈
},
//위의 Symbol.iterator로 반환된 iterator 객체의 next 메서드 호출
next() {
if (this.current <= this.to) {
return { done: false, value: this.current++ };
} else {
return { done: true };
}
}
//range 안에 from, to, iterator 호출, next 호출이 다 들어있다!
};
for (let num of range) {
alert(num); // 1, 2, 3, 4, 5
}
이제 rangeSymbol.iterator가 객체 range 자체를 반환한다.
이렇게 작성하면 더 간결하고 보기 좋지만 이터레이터(객체 자신)가 하나뿐이어서 두 반복문이 반복 상태를 공유하기 때문에 두 개의 for~of 반복문을 하나의 객체에 동시에 사용할 수 없다는 단점이 있다. 물론 동시에 두 개의 for~of를 사용하는 것은 비동기 처리에서도 흔한 케이스는 아니다.
Tip. 무한개의 이터레이터
range에서 range.to에 Infinity를 할당하면 range가 무한대가 된다. 무수히 많은 의사 난수(pseudorandom numbers)를 생성하는 이터러블 객체를 만드는 것도 가능하다.
next엔 제약사항이 없고 next가 값을 계속 반환하는 것은 정상적인 동작이다.
물론 위와 같은 이터러블에 for~of 반복문을 사용하면 끝이 없겠지만 break를 사용하면 언제든지 반복을 멈출 수 있다.
문자열 또한 이터러블이다.
for~of는 문자열의 각 글자를 순회한다.
for (let char of "test") {
// 글자 하나당 한 번 실행(4회 호출).
alert( char ); // t, e, s, t가 차례대로 출력됨
}
서로게이트 쌍(surrogate pair)에도 잘 동작한다.
서로게이트 쌍은 간략하게 설명하면 16비트 코드 두개로 문자 하나를 표현한 것을 의미한다.
(참고 출처: http://jays1204.github.io/encoding/charset/2015/03/23/surrogate-pair.html)
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;
alert(result.value); // 글자가 하나씩 출력된다.
}
이터레이터를 명시적으로 호출하는 경우는 거의 없는데, 이 방법을 사용하면 for~of를 사용하는 것보다 반복 과정을 더 잘 통제할 수 있다는 장점이 있다. 반복을 시작했다가 잠시 멈춰 다른 작업을 하다가 다시 반복을 시작하는 것과 같이 반복 과정을 여러 개로 쪼개는 것이 가능하다.
이터러블과 유사 배열은 비슷해 보이지만 이 둘은 많이 다른 개념이다. 헷갈리지 않으려면 두 용어를 잘 이해하고 있어야 한다.
이터러블(iterable) - 메서드 Symbol.iterator가 구현된 객체.
유사 배열(array-like) - 인덱스와 length 프로퍼티가 있어서 배열처럼 보이는 객체.
대표적인 예시로는 이터러블 객체(for~of 를 사용할 수 있음)이면서 유사배열 객체(숫자 인덱스와 length 프로퍼티가 있음)인 문자열이 있다.
이터러블 객체가 유사 배열 객체인 건 아니고 유사 배열 객체가 이터러블 객체인 것도 아니다.
위 예시의 range 또한 이터러블 객체이긴 하지만 인덱스도 없고 length 프로퍼티도 없으므로 유사 배열 객체가 아니다.
아래 예시의 객체는 유사 배열 객체이지만 이터러블은 아니다.
let arrayLike = { // 인덱스(key)와 length프로퍼티가 있음 => 유사 배열
0: "Hello",
1: "World",
length: 2
};
// Symbol.iterator가 없으므로 에러 발생
for (let item of arrayLike) {}
이터러블과 유사 배열은 대개 배열이 아니기 때문에 push, pop 등의 메서드를 지원하지 않는다. 어떻게 하면 이터러블과 유사 배열에 배열 메서드를 적용할 수 있을까?
범용 메서드 Array.from는 이터러블이나 유사 배열을 받아 ‘진짜’ Array를 만들어준다.
이 과정을 거치면 이터러블이나 유사 배열에 배열 메서드를 사용할 수 있다.
유사배열에 Array.from을 사용한 예시:
// 유사배열
let arrayLike = {
0: "Hello",
1: "World",
length: 2
};
let arr = Array.from(arrayLike);
alert(arr.pop()); // World (pop 메서드가 정상동작)
위 예시에서 쓰인 Array.from은
1) 객체를 받아 이터러블이나 유사 배열인지 조사
2) 넘겨 받은 인수가 이터러블이나 유사 배열인 경우, 새로운 배열을 만들고 객체의 모든 요소를 새롭게 만든 배열로 복사
이터러블에 Array.from을 사용한 예시:
// 이터러블 예시로 나온 이터러블 객체 range ({from:1, to:5})
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 ({from:1, to:5})
// 각 숫자를 제곱
let arr = Array.from(range, num => num * num);
alert(arr); // 1,4,9,16,25
//------------문자열을 배열로 만드는 예시--------------
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을 사용하면 서로게이트 쌍을 처리할 수 있는 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) ); // 이상한 값이 출력됨