iterable을 직역하면 반복, 순회 가능이다. 이터러블은 반복 가능한 객체를 의미한다. 이를 순회하며 요소에 접근 할 수 있다.
이터러블 프로토콜은 반복, 순회 기능을 사용하는 주체간의 통일된 규격을 의미한다. 즉, 이터러블 객체들이 구현해야 하는 메서드를 규정한 규격이다.
이터러블은 이터러블 프로토콜을 준수하는 객체를 의미한다. 예를 들어 배열, 문자열, Map, Set 등과 같은 자료형이 이터러블이다.
그렇다면 이터러블을 판단하는 기준은 무엇일까? 이터러블 객체들은 키 Symbol.iterator의 값으로 이터레이터를 변환하는 메서드를 갖는다.
확인해보자.
console.log([]);
/*
length: 0
[[Prototype]]: Array(0)
...
Symbol(Symbol.iterator): ƒ values()
...
*/
빈 배열을 출력하고, 프로토타입을 확인해 보면 Symbol(Symbol.iterator): ƒ values()프로퍼티가 존재한다. 이 프로퍼티가 이터러블의 핵심내용을 담고 있다. 다른 이터러블의 Symbol.iterator을 출력해보자
console.log(
[][Symbol.iterator],
''[Symbol.iterator],
new Set()[Symbol.iterator],
new Map()[Symbol.iterator]
);
/*
ƒ values() { [native code] }
ƒ [Symbol.iterator]() { [native code] }
ƒ values() { [native code] }
ƒ entries() { [native code] }
*/
// 다른 타입의 인스턴스에는 없음
console.log(
(1)[Symbol.iterator],
(true)[Symbol.iterator],
{ x: 1 }[Symbol.iterator]
);
/* undifined undifined undifined*/
어떤 함수들이 나타난다. 이 함수들은 이터러블이 아닌 다른 자료형에는 존재하지 않는다. 함수들을 실행해 보자.
console.log(
[][Symbol.iterator](),
''[Symbol.iterator](),
new Set()[Symbol.iterator](),
new Map()[Symbol.iterator]()
);
/*
Array Iterator {}
StringIterator {}
SetIterator {}
MapIterator {}
*/
이터러블 객체들의 Symbol.iterator 함수들을 실행하면 Array Iterator, String Iterator등 객체 타입에 따른 Iterator라는 객체를 반환하는 모습을 확인할 수 있다.
만약 내가 어떤 객체를 이터러블로 만들려면
Symbol.iterator키에 특정함수를 넣어줌으로써 이터러블 프로토콜을 준수하는 객체로 만들 수 있다.
내가 원하는 이터러블을 만들 수 있다는 이야기다.
그럼 Iterator객체는 뭘까? 배열을 먼저 살펴보자
const arr = [1, 'A', true, null, {x: 1, y: 2 }];
const arrIterator = arr[Symbol.iterator]();
console.log(arrIterator);
/*
Array Iterator {}
[[Prototype]]: Array Iterator
next: ƒ next()
Symbol(Symbol.toStringTag): "Array Iterator"
[[Prototype]]: Object
Symbol(Symbol.iterator): ƒ [Symbol.iterator]()
[[Prototype]]: Object
*/
Array Iterator프로토타입으로 next라는 함수가 존재한다.
호출해보자
console.log(arrIterator.next());
console.log(arrIterator.next());
console.log(arrIterator.next());
/*
{value: 1, done: false}
{value: 'A', done: false}
{value: true, done: false}
*/
next를 반복 실행하였더니 배열에 저장된 값들을 처음부터 차례차례 반환하고 있다. 그런데 값만 반환하는 것이 아니라 done이라는 프로퍼티를 포함한 객체를 반환한다.
배열이 끝날때 까지 next를 호출해보자
console.log(arrIterator.next());
console.log(arrIterator.next());
console.log(arrIterator.next());
console.log(arrIterator.next());
/*
{value: null, done: false}
{value: {x: 1, y: 2 }, done: false}
{value: undefined, done: true}
{value: undefined, done: true}
*/
배열의 마지막 요소를 반환하고 또 다시 next를 호출하였더니 done의 값이 true로 바뀌고 값은 undifined로 출력된다. 반복 호출을 하여도 같은 결과를 출력한다.
done은 해당 이터러블의 모든 요소를 방문했는지를 나타내고, value에는 이번에 방문한 값을 저장한다.
즉, next메서드는 이터러블의 요소를 처음부터 차례대로 방문하고, 모든 요소를 방문했을 때는 더 이상 이터러블의 값을 반환하지 않는다.
이터러블을 만드려면 이런 형식을 지키면 된다.
const obj = {
// ⭐️ 아래의 메서드를 갖는 것이 이터러블 프로토콜
[Symbol.iterator] () {
// code...
length = 5
curr = 0
// ⭐️ 이터레이터(next 메서드를 가진 객체)을 반환
return {
next () {
return {
value: curr,
done: curr++ < 12
}
}
}
}
}
const objIterator = obj[Symbol.iterator]();
for (let i = 0; i < 5; i++) {
console.log(
objIterator.next()
);
}
/*
{value: 0, done: true}
{value: 1, done: true}
{value: 2, done: true}
{value: 3, done: true}
{value: 4, done: true}
*/
function* genFunction () {
console.log('하나를 반환합니다.');
yield '하나';
console.log('둘을 반환합니다.');
yield '둘';
console.log('셋을 반환합니다.');
yield '셋';
}
낯선 형태의 함수다. function앞에 *가 붙어있고, yield라는 처음보는 키워드가 존재한다.
일단 호출해보자
genFunction();
/*
genFunction {<suspended>}
*/
❓❓.. 나는 분명 함수를 호출했는데 객체가 반환되었다. 살펴보자
genFunction {<suspended>}
[[Prototype]]: Generator
[[Prototype]]: Generator
constructor: GeneratorFunction {prototype: Generator, Symbol(Symbol.toStringTag): 'GeneratorFunction', constructor: ƒ}
next: ƒ next()
return: ƒ return()
throw: ƒ throw()
Symbol(Symbol.toStringTag): "Generator"
[[Prototype]]: Object
[[GeneratorState]]: "suspended"
[[GeneratorFunction]]: ƒ* genFunction()
[[GeneratorReceiver]]: Window
[[Scopes]]: Scopes[3]
이터레이터 처럼 next메서드와 Symbol(Symbol.toStringTag): "Generator"를 가지고 있는 모습이다. 이터레이터처럼 반복 호출을 해보자.
const genFunc = genFunction();
console.log(genFunc.next());
console.log(genFunc.next());
console.log(genFunc.next());
console.log(genFunc.next());
/*
하나를 반환합니다.
{value: '하나', done: false}
둘을 반환합니다.
{value: '둘', done: false}
셋을 반환합니다.
{value: '셋', done: false}
{value: undefined, done: true}
*/
우리가 함수를 생각할 때 함수를 호출하면 함수의 모든 코드가 연속적으로 실행된다고 생각한다.
하지만 함수를 호출했을 때에는 객체를 반환했고, 그 객체에 포함된 next메서드를 호출하자 yeild라는 키워드를 만나면 함수 실행을 멈추고 마치 이터레이터처럼 {value: '하나', done: false}라는 객체를 반환한다.
함수가 할 일을 실행하는 것이 아닌 함수를 사용하는 호출자에게 자신의 실행권한을 양도한다.
보다시피 이터레이터의 기능을 가지고 있는데 문법은 보다 직관적이다.
yeild를 만나면 멈춤, value와 done을 갖는 객체가 함수의 마지막 실행을 만날 때 까지 반환됨
이처럼 제너레이터는 이터러블과 이터레이터의 기능을 보다 간결하게 구현 가능하다. 아까 만들었던 이터러블을 제너레이터로 사용해서 변환해보자.
function* getGenerator () {
length = 5
curr = 0
while(curr++ < length){
yield curr
}
const gener = getGenerator();
for (let i = 0; i < 5; i++) {
console.log(
gener.next()
);
}
/*
{value: 0, done: true}
{value: 1, done: true}
{value: 2, done: true}
{value: 3, done: true}
{value: 4, done: true}
*/
⭐ 제너레이터를 이터러블로 사용하면 한번 순회 후에는 새롭게 객체를 만들어 줘야한다.
for (let i = 0; i < 5; i++) {
console.log(
gener.next()
);
}
/*
{value: 0, done: true}
{value: 1, done: true}
{value: 2, done: true}
{value: 3, done: true}
{value: 4, done: true}
*/
for (let i = 0; i < 5; i++) {
console.log(
gener.next()
);
}
/*
{value: undefined, done: true}
{value: undefined, done: true}
{value: undefined, done: true}
{value: undefined, done: true}
{value: undefined, done: true}
*/