이터레이터(iterator
) 란?
next()
메서드가 있는 객체이다.next()
를 호출할 때마다 시퀀스의 다음 값과 완료 여부를 나타내는 플래그를 반환한다.- ex)
{value: 10, done: false}
- ex)
이터러블(iterable
) 이란?
- 이터레이터를 가져오는 표준 메서드가 있는 객체이다.
Symbol.iterator
프로퍼티에서iterator
를 반환하는 메서드를 구현하면 된다.- 프로퍼티로 얻어오는 것이 아니라, 메서드로 얻어오는 이유는 매번 가장 첫번째 원소를 가리키는
iterator
를 생성하여 반환하고 싶기 때문일 것이라 추측한다.
- 프로퍼티로 얻어오는 것이 아니라, 메서드로 얻어오는 이유는 매번 가장 첫번째 원소를 가리키는
for of
와 iterator
iterator
를 구현하여iterable
객체를 만들면,for of
문에 의해 반복이 가능하다.
const a = ["a", "b", "c"];
for (const x of a) {
console.log(x);
}
- 위의 코드는 실제로
for of
구문이iterator
를 반환받아 반복시키고value
를 반환하는 동작을 가지고 있다.
이터레이터 명시적으로 사용해보기
const a = ["a", "b", "c"];
const it = a[Symbol.iterator]();
console.log("it", it); // Object [Array Iterator] {}
let result = it.next();
console.log("result", result); // { value: 'a', done: false }
while (!result.done) {
console.log(result.value);
result = it.next();
}
a[Symbol.iterator]()
를 통해 이터레이터를 가져올 수 있다.- ts 에서는 Iterator 인터페이스 를 따른다.
next()
,return()
,throw()
등의 메서드 이용이 가능하다.- 메서드의 반환 값은 주로
IteratorResult
이다.
interface Iterator<T> {
next(value?: any): IteratorResult<T>;
return?(value?: any): IteratorResult<T>;
throw?(e?: any): IteratorResult<T>;
}
interface IteratorResult<T> {
done: boolean;
value: T;
}
자바스크립트 배열의 이터레이터 (iterator
) 살펴보기
const arr = [];
console.log(arr[Symbol.iterator]());
/*
Array Iterator {}
[[Prototype]]: Array Iterator
next: ƒ next()
Symbol(Symbol.toStringTag): "Array Iterator"
[[Prototype]]: Object
*/
이터레이터 (iterator
) 직접 구현해보기
const customArr = {
cur: 0, // current
length: 0,
next() {
if (this.cur < this.length) {
return {
value: this[this.cur++],
done: false,
};
}
return {
value: undefined,
done: true,
};
},
return() {
console.log("return");
return { value: undefined, done: true };
},
[Symbol.iterator]() {
return this;
},
};
customArr[0] = 1;
customArr[1] = 20;
customArr[2] = 3;
customArr[3] = 10;
customArr.length = 4;
for (const e of customArr) {
console.log(e);
}
Array
의 빌트인 메서드인 values()
를 통해 이터레이터 (iterator
) 구현해보기
const customArr = {
0: 1,
1: 5,
length: 2,
[Symbol.iterator]() {
return Array.prototype.values.call(this);
},
};
for-of
를 돌렸을 때, 반대로 순회하는 Array
클래스 만들어보기
만일, 배열의 원소가 10만개 이상이며 프리즈 상태여서 내부의 요소를 수정할 수 없는 상황이면, reverse()
메서드를 통해 배열을 복사하기보다는 원본 배열을 반대로 순회할 수 있게 도와주는 iterator
를 가진 클래스를 만들어 어느정도 이득을 볼 수 있다.
이렇게 Symbol.iterator
를 구현해두면, for-of
뿐만 아니라, 이터레이터를 활용하는 어떠한 메서드에도 이를 적용시킬 수 있다.
class ReversedArray {
constructor(arr) {
this.arr = arr;
}
[Symbol.iterator]() {
let index = this.arr.length - 1;
return {
next: () => {
if (index >= 0) {
return { value: this.arr[index--], done: false };
}
return { value: undefined, done: true };
},
};
}
}
const reArr = new ReversedArray([1, 2, 3]);
for (const e of reArr) {
console.log(e);
}
반복할 수 없는 객체를 반복하려 할 때
const obj = {};
for (const a of obj) {
console.log(a);
}
obj[Symobl.iterator]()
의 반환값이 없는 것을 기준으로 반복할 수 없는 객체라는 것을 판단한다.Uncaught TypeError: obj is not iterable
라는 예외를 출력해준다.
for-of
와 for-in
의 차이
얼핏 보기엔 비슷한 역할을 수행할 것 같은데, 두 구문에는 명확한 차이가 있다.
const a = ["a", "b", "c"];
a.extra = "extra property";
for (const value of a) {
console.log(value); // a, b, c
}
for (const key in a) {
console.log(key); // 0, 1, 2, extra
}
for-of
는배열 이터레이터
에 의해 정의된엔트리의 값 (value)
을 제공한다.for-in
은enumerable property
를 순회하며,배열 엔트리 속성 이름 (key)
만 제공한다.
이터레이터는
for-of
에서만 쓰는 건 아니며, 스프레드 구문, 디스트럭처링,Promise.all()
,Array.from()
,Map
,Set
등도 이터레이터를 사용한다.
이터레이터 사용 중 실수 예방하기
done
이 false
인 것 보장하기
const a = ["a", "b", "c"];
const it = a[Symbol.iterator]();
let result;
while (!(result = it.next()).done) {
console.log(result.value);
}
while
안에서it.next()
를 통한 할당을 수행하도록 약속하면, 무분별한 위치에서it.next()
가 수행되는 위험을 줄일 수 있다.iterator.next()
가 반환한 객체의done
이false
라는 보장이 있는 경우에만 로직을 수행한다.it.next()
가 여러군데에서 사용되는 경우 생길 수 있는 오류를 예방한다.
이터레이터 반복 중지하기
이터레이터에서는 몇가지 반복을 중지하는 방법을 제공할 수 있다.
값을 찾았다면 즉시 종료하기 (return()
)
const a = ["a", "b", "c"];
const it = a[Symbol.iterator]();
let result;
while (!(result = it.next()).done) {
if (result.value === "b") {
if (it.return) {
it.return();
}
break;
}
}
- 값을 찾은 시점에서 더이상 루프를 돌지 않고 멈추고 싶다면,
return()
메서드를 사용하면 된다.- 명시적
return()
시에는 리소스를 정리해야 한다고 알리기 때문에, 혹시나라도 사용하지 않는 자원을 들고 있진 않을까 걱정할 필요가 없다.
- 명시적
for-of
는iterator
를 이용한다. 그렇다면for-of
는 중간에break
로 끝났을 때 계속 메모리를 점유할까? 아니다. 탈출 시에return()
메서드를 호출하도록 프로그래밍 되어 있다.
예외 던지기 (throw()
)
iterator
에 예외를 발견했다고 호출자가 알려줄 때 사용한다.- 간단한
iterator
에 잘 사용되진 않는다.
iterator
를 직접 구현해야 하는 경우
거의 없다.
- 이터레이터에 기능을 추가하려는 경우
- 이터레이터를 수동으로 추가해야만 하는 경우 (거의 없다.)
프로토타입을 추가할 때 유의사항
- 추가한 속성/메서드가 열거할 수 없는지 확인한다.
- 향후 추가될 수 있는 기능과 충돌할 가능성이 없는 이름인지 확인한다.
- 대부분의 경우 라이브러리 코드는 프로토타입 수정을 완전히 피해야 한다.
Array Iterator
에 새로운 메서드 추가하기
const iteratorPrototype = Object.getPrototypeOf(
Object.getPrototypeOf([][Symbol.iterator]())
);
Object.defineProperty(iteratorPrototype, "myFind", {
value(callback, thisArg) {
let result;
while (!(result = this.next()).done) {
if (callback.call(thisArg, result.value)) {
break;
}
}
return result;
},
writable: true,
configurable: true,
});
const arr = ["one", "two", "three"];
const it = arr[Symbol.iterator]();
let result;
while (!(result = it.myFind((v) => v.includes("e"))).done) {
console.log("Found: " + result.value);
}
// {value: 'one', done: false}
// {value: 'three', done: false}
위의 코드는 한번에 잘 안 와닿을 수 있는데, 아래와 같은 프로세스를 거쳤다.
[][Symbol.iterator]()
는 빈 배열이 가진 자신의iterator
를 반환한다.- 내부적으로
Array Iterator
를 프로토타입으로 갖는 빈 오브젝트가 들어있다.
- 내부적으로
Object.getPrototypeOf([][Symbol.iterator]())
를 통하면 프로토타입에 있던Array Iterator
를 실제로 접근 가능하다.- 한 번 더
Object.getPrototypeOf(Object.getPrototypeOf([][Symbol.iterator]()));
를 하면, 마침내 모든Iterator
가 프로토타입으로 갖는Symbol(Symbol.iterator)
를 가질 수 있다.- 여기에 메서드를 추가하면, 모든
iterator
에서 공통으로 해당 메서드를 사용할 수 있다.
- 여기에 메서드를 추가하면, 모든
일단 위의 이해를 기본으로 한다. 이후에는
- 최상위
Iterator Prototype
에myFind
라는 프로퍼티를 추가한다. - 이 프로퍼티의 값은 단순
value
가 아닌 메서드 프로퍼티로 지정된다. - 이 메서드 프로퍼티는 콜백 함수를 받고, 콜백 함수가
true
를 반환할 때까지iterator.next()
를 진행한다.- 위의 예에서는 콜백에
includes("e")
를 넣어서,e
가 포함되지 않은 경우 스킵된다. - 조건이 맞는 경우만
break
에 걸려return
이 이뤄진다.
- 위의 예에서는 콜백에
iterator
의 프로토타입을 올바르게 설정하여 구현해보기
const a = {
0: "a",
1: "b",
2: "c",
length: 3,
[Symbol.iterator]() {
let index = 0;
const it = {
next: () => {
if (index < this.length) {
return { value: this[index++], done: false };
}
return { value: undefined, done: true };
},
};
return it;
},
};
next
에서 화살표 함수를 사용하지 않으면,this
가it
자체가 되어버린다.- 위의 코드처럼
next()
메서드를 반환값 인터페이스에 맞게 올바르게 구현만 해도for-of
를 사용하는데는 지장이 없지만, 프로토타입이%IteratorPrototype%
은 아니다. - 위에서 정의했던
myFind()
와 같은 커스텀iterator
메서드 사용이 불가능하다.
const a = {
0: "a",
1: "b",
2: "c",
length: 3,
[Symbol.iterator]() {
let index = 0;
const itPrototype = Object.getPrototypeOf(
Object.getPrototypeOf([][Symbol.iterator]())
);
const it = Object.assign(Object.create(itPrototype), {
next: () => {
if (index < this.length) {
return { value: this[index++], done: false };
}
return { value: undefined, done: true };
},
});
return it;
},
};
- 이제는 프로토 타입이
%IteratorPrototype%
이다. - 프로토 타입이 바뀌며, 기존의 프로토타입이 가지고 있던 메서드들도 다 사용할 수 있게 된다.
- 이전에 정의한
myFind()
메서드도 사용할 수 있다.
- 이전에 정의한
LinkedList
이터레이터로 구현하기
class LinkedList {
constructor() {
this.head = this.tail = null;
}
add(value) {
const entry = { value, next: null };
if (!this.tail) {
this.head = this.tail = entry;
} else {
this.tail = this.tail.next = entry;
}
}
[Symbol.iterator]() {
let current = this.head;
const itPrototype = Object.getPrototypeOf(
Object.getPrototypeOf([][Symbol.iterator]())
);
const it = Object.assign(Object.create(itPrototype), {
next() {
if (current) {
const value = current.value;
current = current.next;
return { value, done: false };
}
return { value: undefined, done: true };
},
});
return it;
}
}
const list = new LinkedList();
list.add("one");
list.add("two");
list.add("three");
for (const e of list) {
console.log(e);
}
부모 엘리먼트를 차례대로 가져오는 iterator
만들어보기
function parents(element) {
return {
next() {
element = element && element.parentNode;
if (element && element.nodeType === Node.ELEMENT_NODE) {
return { value: element, done: false };
}
return { value: undefined, done: true };
},
[Symbol.iterator]() {
return this;
},
};
}
이터러블 스프레드 문법
이터러블 스프레드 문법은 함수를 호출하거나 배열을 생성할 때 결괏값을 이산 값으로 분산하여 이터러블을 소비하는 방법을 제공한다.
배열을 이산 인수로 제공하고 싶을 때
const a = [1, 2, 3, 4, 5, 6];
Math.min(...a);
Math.min(-1, 0, ...a, 7, 8); // 이것도 유효하다
새로운 배열을 생성할 때
const a = [1, 2, 3, 4];
const b = [5, 6, 7, 8];
const c = [...a, ...b];
console.log(c); // [1, 2, 3, 4, 5, 6, 7, 8]
DOM 요소는 iterable
한가?
- 최신 브라우저에서 다중 DOM 요소는
NodeList
라는 타입을 갖는다. (크롬, 파이어폭스, 엣지, 사파리) - WHAT-WG DOM 스펙은
NodeList
를HTMLCollection
이 아니라iterable
로 표시한다. - 결과적으로 DOM 요소에도
for-of
혹은...
같은iterator
를 사용하는 문법을 쓸 수 있다.
DOM iterable
하도록 polyfill
작성해보기
(function () {
if (Object.defineProperty) {
var iterator =
typeof Symbol !== "undefined" &&
Symbol.iterator &&
Array.prototype[Symbol.iterator];
var forEach = Array.prototype.forEach;
var update = function (collection) {
var proto = collection && collection.prototype;
if (proto) {
if (iterator && !proto[Symbol.iterator]) {
Object.defineProperty(proto, Symbol.iterator, {
value: iterator,
writable: true,
configuration: true,
});
}
if (forEach && !proto.forEach) {
Object.defineProperty(proto, "forEach", {
value: forEach,
writable: true,
configuration: true,
});
}
}
};
if (typeof NodeList !== "undefined") {
update(NodeList);
}
if (typeof HTMLCollection !== "undefined") {
update(HTMLCollection);
}
}
})();
브라우저에 따라 collection
의 prototype
에 iterator
가 구현이 안되있거나, forEach
가 구현되어 있지 않다면, polyfill
을 통해 구현해주는 내용이다.
제너레이터 함수
- 작업 중간에 일시 정지가 가능하다.
- 멋진 점은 일시정지된 상태로 진행된 작업 정보를 기억하고 있는 것이다.
- 값을 생성하고 선택적으로 새 값을 받아들인 다음 필요한 만큼 계속 진행할 수 있다.
- 내부적으로 제너레이터 함수는 제너레이터 객체를 만들고 반환한다.
- 이터레이터는 값만 생성하는 반면, 제너레이터는 값을 생성하고 소비할 수 있다.
- 제너레이터 객체 수동 생성도 가능하지만 보통
function *
문법으로 단순화하여 생성한다.
기본 제너레이터 함수 생성하기
function* simple() {
for (let n = 1; n <= 3; ++n) {
yield n;
}
}
const it = simple();
it.next(); // {value: 1, done: false}
it.next(); // {value: 2, done: false}
for (const e of simple()) {
console.log(e); // 1, 2, 3
}
console.log(it[Symbol.iterator]());
/*
simple {<suspended>}
[[GeneratorLocation]]: VM43:1
[[Prototype]]: Generator
[[GeneratorState]]: "suspended"
[[GeneratorFunction]]: ƒ* simple()
[[GeneratorReceiver]]: Window
*/
simple()
의 결과는 제너레이터 객체이다.next()
메서드를 수행하면,yield
로 값을 반환할 때까지 코드를 수행한다.yield
를 만나지 못하면,{value: undefined, done: true}
를 반환한다.
제너레이터로 iterator
구현하기
const a = {
0: "a",
1: "b",
2: "c",
length: 3,
[Symbol.iterator]: function* () {
for (let index = 0; index < this.length; ++index) {
yield this[index];
}
},
};
for (const value of a) {
console.log(value);
}
- 제너레이터를 사용하지 않고 구현하는 것보다 훨씬 간단하다.
LinkedList
클래스의 iterator
를 제너레이터 이용해서 구현해보기
class LinkedList {
constructor() {
this.head = this.tail = null;
}
add(value) {
const entry = { value, next: null };
if (!this.tail) {
this.head = this.tail = entry;
} else {
this.tail = this.tail.next = entry;
}
}
*[Symbol.iterator]() {
for (let cur = this.head; cur !== null; cur = cur.next) {
yield cur.value;
}
}
}
const list = new LinkedList();
list.add("one");
list.add("two");
list.add("three");
for (const e of list) {
console.log(e);
}
*[Symbol.iterator](){ ... }
메서드명 앞에*
을 붙여주는 방법을 사용할 수 있다.
제너레이터로 값 소비하기
- 제너레이터는 이터레이터와 다르게 '값을 소비할 수 있다.'
- 값을 소비한다는 것은
next()
메서드에 값을 받아 로직에 이용한다는 뜻이다.
function* tellMeAboutYou() {
const name = yield "what is your name?";
console.log(`oh your name is ${name}`);
const age = yield "how old are you?";
console.log(`oh now you are ${age}`);
}
const gt = tellMeAboutYou();
console.log(gt.next());
// what is your name?
console.log(gt.next("jake seo"));
// oh your name is jake seo
// how old are you?
gt.next("5");
// oh now you are 5
- 처음부터 무언가 값을 넘기고 싶다면, 제너레이터 함수의 인자를 이용하자.
제너레이터의 용도
- 기본적으로 상태머신이다.
- 사용자로부터 값을 무한대로 입력받고, 무조건 마지막 세 수의 합을 구하는 경우 유용하다.
- 심리테스트 등 사용자가 입력한 정보 값을 기반으로 다음 값을 결정해야 하는 경우 유용하다.
function* sumLastThree() {
let arr = [];
while (true) {
if (arr.length < 3) {
const num = yield `input ${3 - arr.length} more numbers`;
arr.push(num);
continue;
}
const num = yield arr.reduce((a, c) => a + c, 0);
arr = [...arr.slice(-2), num];
}
}
const gt = sumLastThree();
console.log(gt.next()); // {value: 'input 3 more numbers', done: false}
console.log(gt.next(1)); // {value: 'input 3 more numbers', done: false}
console.log(gt.next(2)); // {value: 'input 3 more numbers', done: false}
console.log(gt.next(3)); // {value: 6, done: false}
console.log(gt.next(4)); // {value: 9, done: false}
console.log(gt.next(100)); // {value: 107, done: false}
제너레이터의 return
function* usingReturn() {
yield 1;
yield 2;
return 3;
}
for (const v of usingReturn()) {
console.log(v);
}
// 1
// 2
// NOT LOGGED -> {value: 3, done: true}
- 제너레이터의
return
은{done: true}
인 오브젝트를 반환한다. for-of
문은done: true
인 경우의value
는 보지 않기 때문에, 출력되지 않는다.
yield
의 낮은 우선 순위
function* example() {
let a = yield +2 + 30;
return a;
}
yield
의 낮은 우선순위 때문에,yield
로 받은 값에2
와30
이 더해지는 것이 아니라,+2+30
이 먼저 계산되고let a = yield 32
와 같이 해석된다.
function* example() {
let a = yield* 2 + 30;
return a;
}
*2+30
은 올바른 연산이 아니므로 에러가 발생한다.
function* example() {
let a = yield 2 + 30 + yield;
return a;
}
- 아예 문법 에러가 난다.
function* example() {
let a = (yield) + 2 + 30;
return a;
}
- 괄호에 있는 것이 먼저 해석되기 때문에, 이는 올바른 문법이다.
yield
의 해석을 먼저 진행한다.
제너레이터의 throw
function* example() {
yield 1;
yield 2;
yield 3;
}
const gen = example();
console.log(gen.next()); // {value: 1, done: false}
console.log(gen.throw(new Error("boom"))); // Uncaught Error: boom
console.log(gen.next()); // not executed.
function* example() {
while (true) {
try {
yield 1;
yield 2;
yield 3;
} catch (e) {
console.error("generator boom!");
yield "error";
}
}
}
const gen = example();
console.log(gen.next()); // {value: 1, done: false}
console.log(gen.throw(new Error("boom"))); // {value: 'error', done: false}
console.log(gen.next()); // {value: 1, done: false}
제너레이터와 이터러블 넘겨주기: yield*
function* collect(count) {
const data = [];
if (count < 1 || Math.floor(count) !== count) {
throw new Error("count must be an integer >= 1");
}
do {
let msg = "values needed: " + count;
data.push(yield msg);
} while (--count > 0);
return data;
}
function* outer() {
let data1 = yield* collect(2);
console.log("data collected by collect(2) =", data1);
let data2 = yield* collect(3);
console.log("data collected by collect(3) =", data2);
return [data1, data2];
}
const outerG = outer();
console.log("next got:", outerG.next());
console.log("next got:", outerG.next("a"));
console.log("next got:", outerG.next("b"));
console.log("next got:", outerG.next("c"));
console.log("next got:", outerG.next("d"));
console.log("next got:", outerG.next("e"));
yield*
키워드를 통해 제너레이터를 넘겨주었다.- 이를 이용해 내부의 다른 제너레이터 상태머신을 이용할 수 있다.
내부 제너레이터 실행 중 return
하기
function* inner() {
try {
let n = 0;
while (true) {
yield "inner " + n++;
}
} finally {
console.log("inner terminated");
}
}
function* outer() {
try {
yield "outer before";
yield* inner();
yield "outer after";
} finally {
console.log("outer terminated");
}
}
const gen = outer();
let result = gen.next();
console.log(result);
result = gen.next();
console.log(result);
result = gen.next();
console.log(result);
result = gen.return(42);
console.log(result);
result = gen.next();
console.log(result);
/*
{value: 'outer before', done: false}
{value: 'inner 0', done: false}
{value: 'inner 1', done: false}
inner terminated
outer terminated
{value: 42, done: true}
{value: undefined, done: true}
*/
return
예제 2: 바로 return
하면?
function foo() {
try {
return "a";
} finally {
return "b";
}
}
console.log(foo()); // b
return
을 하는 순간finally()
로 넘어가기 때문에 바로"b"
가 반환된다.
return
예제 3: return
으로 값 오버라이드
function* foo(n) {
try {
while (true) {
n = yield n * 2;
}
} finally {
return "override";
}
}
const gen = foo(2);
console.log(gen.next()); // {value: 4, done: false}
console.log(gen.next(3)); // {value: 6, done: false}
console.log(gen.next(4)); // {value: 8, done: false}
console.log(gen.return(4)); // {value: "override", done: true}
throw
해보기
function* inner() {
try {
yield "something";
console.log("inner - done");
} finally {
console.log("inner - finally");
}
}
function* outer() {
try {
yield* inner();
console.log("outer - done");
} finally {
console.log("outer - finally");
}
}
const gen = outer();
let result = gen.next();
result = gen.throw(new Error("boom"));
/*
inner - finally
outer - finally
Uncaught Error: boom
*/
액션 플랜
이터러블 소비 구문을 활용해보자
index
가 필요하다면, 기존의for
문을 사용하자.index
가 필요 없으면,for-of
를 사용하자.- 콜백을 여러번 활용할 필요가 있다면,
forEach
도 좋은 선택이다.
DOM 컬렉션 반복 기능 사용
for-of
를 통해 DOM 객체를 반복해보자.Array.prototype.slice.call()
과 같은 메서드로 DOM 을 바인딩해Array
의 메서드를 이용하는 것도 가능하다.
이터러블, 이터레이터 인터페이스 사용
- 새 클래스나 객체에 반복이 필요한 경우,
[Symbol.iterator]()
를 정의해보자.
이터러블 스프레드 구문을 사용하자
Math.max.apply("", [1, 2, 3, 1111]);
는 가독성이 좋지 않다.Math.max(...[1, 2, 3, 1111]);
이 더 가독성이 좋다.
제너레이터 사용하기
- 상태 머신이 필요한 경우 제너레이터를 사용했을 때, 더 명확한 코드를 작성할 수도 있다.
- 단, 코드 흐름 문법으로 더 잘 모델링되지 않은 상태 머신인 경우에는 제너레이터가 올바른 선택이 아닐 수도 있다.
'자바스크립트 > 모던 자바스크립트' 카테고리의 다른 글
모던 자바스크립트, async await (0) | 2023.02.28 |
---|---|
모던 자바스크립트, 제너레이터 (Generator) (0) | 2023.02.28 |
모던 자바스크립트, 프라미스 2 - 유틸 메서드와 작업 패턴 그리고 안티 패턴 (0) | 2023.02.15 |
모던 자바스크립트, 프라미스 1 - 기본 개념 (0) | 2023.02.15 |
모던 자바스크립트, 디스트럭처링 (Desctructuring) (0) | 2023.02.08 |