리스트의 순회는 상당히 중요하다. ES6에서부터 새롭게 생긴 방식은 아래와 같다.
//AS-Was
const list = [1, 2, 3, 4]
for(let i = 0; i < list.length; i++){
console.log(list[i]);
}
//To-BE
for(const a of list){
console.log(a)
}
위 두가지는 전혀 다른 방식임을 먼저 말하고 시작하겠다.
for _ of _
의 경우 Symbol.iterator
라는 심볼을 통해서 접근하는 방식이다.
이터레이터의 경우 기본적으로 next 함수 하나만 있다고 생각하면 된다. 이해가 안 갈 것이다. 아래 예제 코드를 확인해보자
const array = [1, 2, 3, 4];
const iterator = (() => {
let num = 1;
return {
next: () => {
return num > 4 ? { done: true } : { done: false, value: num++ };
},
};
})();
/*
console.log(array); //[1, 2, 3, 4]
console.log(iterator.next().value); // 1
console.log(iterator.next().value); // 2
console.log(iterator.next().value); // 3
console.log(iterator.next().value); // 4
*/
next 함수를 통해서 done, value 두 항목을 전달하는 간단한 로직이다.
1부터 4까지 리스트에 담고 있는 방식이 아닌, num 이라는 메모리에 작은 한칸의 공간의 데이터값을 증가시키면서 가져오고 있다.
직관적으로도 굉장히 큰 리스트(피보나치수열같은) 대신 사용함으로써, 메모리를 아낄 수 있다.
예를 들어 이미지 분석을 위해 특정 디렉토리에 있는 이미지들을 불러와 미리 작성해둔 코드를 돌려가셔 처리해야 하는 작업(딥러닝에 있어서 전처리 과정) 에서 이터레이터를 통해 하나의 메모리만 올려놓고 접근해서 사용할 수도 있다.
Iterable 은 반복 가능한 객체를 의미한다. Iterable한 객체는 for _ of _
Spread syntax
Destructuring
을 사용할 수 있다고 한다. 이후에 더 자세히 알아보겠다.
Iterable 하기 위해선 2가지를 충족 시켜야 한다.
1. 객체 내에 Symbol.iterator 메서드를 가지낟.
2. [Symbol.iterator] 메서드는 Iterator 객체를 반환해야한다.
const iterable = {
[Symbol.iterator]() {
return someIteratorObject
}
...
}
for(item of iterable) {
console.log(item) // work
}
Iterator 는 Iterable 객체에서 반복을 실행하는 반복기를 뜻한다. Iterable 객체가 반복하면서 어떠한 값을 반환 할지 결정하는 역할을 한다.
아래 4가지를 준수해야 한다.
1. 객체 내에 next() 가 필요
2. next 는 IteratorResult 를 반환해야한다
3. IteratorResult = {done : boolean, value : any} 의 형태를 띈다.
4. done 값이 true 를 반환했다면, 이후 호출에 대한 done 값도 true 여야 한다.
const iterable = {
[Symbol.iterator]() {
let i = 0
// iterator 객체
return {
next() {
while(i < 10) { // i가 10이 될 때까지 반복기 수행
return { value: i++, done: false }
}
return { done: true } // i 가 10이 되면 반복 종료(value 값 생략 가능)
}
}
}
}
for(let num of iterable) console.log(num) // 0, 1, ..., 9
Iterator 이면서 Iterable인 객체를 Well-formed iterable 이라고 한다. 아래의 폼을 맞춰주는 것을 의미한다.
const wellFormedIterable = { // Iterator 객체
next() {
return someIteratorResultObject
}
// Iterator 객체에 Symbol.iterator 메서드가 존재하며,
// 해당 메서드가 자기 자신(iterator)을 반환한다.
[Symbol.iterator]() {
return this
}
...
}
well_formed 한 iterator 의 경우 자기 자신의 상태를 기억 할 수 있다는 것이 장점이다.
//Arr
const arr = [1,2,3]
for(const a of arr ) console.log(a)
//Set
const set = new Set([1,2,3])
for(const a of set ) console.log(a)
//Map
const map = new Map ([['a', 1], ['b' ,2], ['c',3]])
for(const a of map.keys() log(a) // a, b, c
for(const a of map.values()log(a) //1, 2, 3
for(const a of map.entries() log(a)//['a', 1], ['b' ,2], ['c',3]
위와 같은 방식으로 다양한 자료구조에서 사용가능하다. 전부 Symbol.iterator 가 추가되서 가능하다고 한다.
const iterable = {
[Symbol.iterator](){
let i = 3
return{
next() {
return i==0 ? {done: true }:{value:i--, done: false}
},
[Symbol.iterator]() { return this } // well-formed iterable
}
}
}
let iterator = iterable[Symbol.iterator]()
for (const a of iterable) log(a)
아래와 같은 경우에서도 Iterator/Iterable 한 방식이라고 한다.
const a = [1, 2]
console.log(...a) // 1 2
console.log([...a...[3, 4]] // 1, 2, 3, 4
참고로 ...input 인자를 통해서는 배열을 전달할 수 있고, 이것이 iterator/iterable 한 well-formed Iterable 객체가 된다는 뜻이다.
사실 well-formed-iterable 은 상태를 기억하고, 메모리를 아낄 수 있다는 장점이 있는데 구현하기가 여간 까다롭지 않을 수가 없다. 그래서 나온 방식이 Generator 이다. 한마디로 Iterable + Iterator 를 더욱 쉽게 만들어준다. 형식은 아래와 같다.
function* generatorFunction() {
yield 42;
}
const generator = generatorFunction();
generator === generator[Symbol.iterator]();
// 이 말인 즉슨, 제너레이터의 이터러블은 다음과 같은 방식으로 구현되어 있을 거라는 것을 암시한다.
// generator[Symbol.iterator] = () => this;
yield
는 제너레이터 함수의 실행을 일시 정지시키고 잠시 결과값을 caller 에게 결과를 반환해준다. 여기서 상태를 기억하고 있으며 next 를 통해 그 다음 결과를 yield 해서 받아올 수 있다. 즉 상태를 기억하고 있다는 점이 동일하다.
function* increment() {
console.log('[ENTERED]');
let i = 0;
try {
while (true) {
yield i++;
}
} catch (e) {
console.log('[ERROR]', e);
}
}
const withReturn = increment();
console.log(withReturn.next());
console.log(withReturn.next());
console.log(withReturn.next());
console.log(withReturn.next());
console.log(withReturn.return(42));
// [ENTERED]
// { value: 0, done: false }
// { value: 1, done: false }
// { value: 2, done: false }
// { value: 3, done: false }
// { value: 42, done: true }
function* testGenerator() {
yield 1;
const val = yield 2;
const final = yield 3;
return val; // customize this return value outside
}
const testGeneratorIt = testGenerator();
console.log(testGeneratorIt.next()); // { value: 1, done: false }
console.log(testGeneratorIt.next()); // { value: 2, done: false }
console.log(testGeneratorIt.next(100)); // { value: 3, done: false }
console.log(testGeneratorIt.next()); // { value: undefined -> 100, done: true }
function* testGenerator() {
yield 1;
const val = yield 2;
const final = yield 3;
return final; // customize this return value outside
}
const testGeneratorIt = testGenerator();
console.log(testGeneratorIt.next()); // { value: 1, done: false }
console.log(testGeneratorIt.next()); // { value: 2, done: false }
console.log(testGeneratorIt.next()); // { value: 3, done: false }
console.log(testGeneratorIt.next(100)); // { value: undefined -> 100, done: true }
yield 의 순서와 next(input) 의 순서를 잘 보면 감을 잡을 수 있다.