2023.06.16
for-of
가 이터레이터를 암시적으로 사용하는 데 사용할 수 있는 유일한 언어 기능은 아니다. 이터러블 스프레드 구문이, 디스트럭처링,Promise.all
,Array.from
, 맵과 세트 등을 비롯하여 이터러블을 사용하는 새로운 기능이 몇 가지 있다.
for-of
는 for-in
과 다르다.for-in
을 사용하면 배열 엔트리에 대한 속성 이름인 0
, 1
, 2
가 차례로 나타난다.
for-of
는 배열의 이터레이터에 의해 정의된 엔트리의 값을 제공하는데, for-of
가 배열에 추가했을 수 있는 다른 속성의 값이 아니라 배열 엔트리의 값만 제공한다는 것을 의미한다.(배열은 객체이다)const a = ['a', 'b', 'c'];
a.extra = 'extra property';
for (const value of a) {
console.log(value);
}
// The above produces "a", "b", and "c"
for (const key in a) {
console.log(key);
}
// The above produces "0", "1", "2", and "extra"
for-of
또는 for-in
루프에서도 마찬가지다. 하지만 for-of
/ for-in
에서는 변수의 값이 루프 문에 의해 수정되지 않으므로 원하는 경우 const
로 선언할 수 있다. (let 또는 var도 괜찮다.)for-of
는 매우 편리하여 많이 사용할 것이다.for-of
는 모든 것을 수행하기 때문에 이터레이터의 세부 사항을 이해하는 데 별로 도움이 되지 않는다.const a = ['a', 'b', 'c'];
const it = a[Symbol.iterator](); // Step 1
let result = it.next(); // Step 2
while (!result.done) {
// Step 3.a
console.log(result.value); // Step 3.b
result = it.next(); // Step 3.c
}
Symbol.iterator
메서드를 제공한다. 이 메서드를 호출하여 이터레이터를 가져온다.next
함수를 호출하여 결과 객체를 가져온다. 결과 객체는 IteratorResult
인터페이스를 준수하는 객체이다. 이는 본질적으로 이터레이터가 반복 완료되었는지 여부를 나타내는 done
속성과 현재 반복에 대한 값을 포함하는 value
속성을 가지고 있음을 의미한다.result = it.next()
호출을 코딩하면 사소한 코드 유지 관리 위험이 발생한다는 사실을 알고 있을 것이다. 아래처럼 한 곳에서 처리할 수도 있다.const a = ['a', 'b', 'c'];
const it = a[Symbol.iterator]();
let result;
while (!(result = it.next()).done) {
console.log(result.value);
}
return
이라는 선택적 메서드를 호출하여 중지할 수 있다.const a = ['a', 'b', 'c', 'd'];
const it = a[Symbol.iterator]();
let result;
while (!(result = it.next()).done) {
if (result.value === 'c') {
if (it.return) {
it.return();
}
break;
}
console.log(result.value);
}
return
은 일반적으로 true
로 설정된 결과 객체를 반환해야 한다.return
에 전달하면 결과 객체의 value
는 일반적으로 해당 인수의 값이어야 하지만 요구 사항이 적용되지 않는다고도 말한다.return
이 있는 경우 이러한 방식으로 작동하지만 그렇게 하는 타사 라이브러리의 이터레이터는 믿을 수 없다.for-of
를 사용하는 경우 이터레이터에서 return을 호출하는 방법이 궁금할 수 있다.const a = ["a", "b", "c", "d"];
for (const value of a) {
if (value === "c") {
if(???.return()){
???.return();
}
break; // 괜찮다. 이터레이터의 return을 호출한다.
}
}
const a = ["a", "b", "c", "d"];
for (const value of a) {
if (value === "c") {
break; // 괜찮다. 이터레이터의 return을 호출한다.
}
console.log(value);
}
throw
다.for-of
는 이를 호출하지 않는다.next
메서드를 제공하는 프로토타입 객체에서 상속한다.next
메서드를 제공하는 배열 이터레이터 프로토타입 객체에서 상속한다.표준 런타임에서 이들에 대한 직접 참조를 제공하는 공개적으로 정의된 전역 또는 속성은 없지만 배열 이터레이터 같은 원하는 유형의 이터레이터에서
Object.getPrototype0f
를 사용하여 쉽게 참조를 가져올 수 있다.
// Adding it
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,
});
// Using it
const a = ['one', 'two', 'three', 'four', 'five', 'six'];
const it = a[Symbol.iterator]();
let result;
while (!(result = it.myFind((v) => v.includes('e'))).done) {
console.log('Found: ' + result.value);
}
console.log('Done');
Symbol.iterator
메서드를 제공하고, 이터레이터 프로토타입 속성을 상속하면 된다.// Basic iterator example when not using a generator function
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;
},
};
for (const value of a) {
console.log(value);
}
Symbol.Iterator
함수를 배치하는 것이다.// Basic iterator example on a class when not using a generator function
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 value of list) {
console.log(value);
}
next
는 this
를 사용하지 않기 때문에 화살표 함수를 참조하는 속성이 아닌 메서드 문법을 사용하여 정의할 수 있다.this
를 반환하는 Symbol.iterator
메서드를 제공하기 때문이다.<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Iterable Iterator Example</title>
</head>
<body>
<div>
<span>
<em id="target">Look in the console for the output</em>
</span>
</div>
<script>
// Symbol.iterator 메서드를 구현하면
// 어떻게 for-of 와 함께 반복자를 사용할 수 있는지 보여 준다.
function parents(element) {
return {
next() {
element = element && element.parentNode;
if (element && element.nodeType === Node.ELEMENT_NODE) {
return { value: element };
}
return { done: true };
},
// 이 이터레이터를 이터러블로 만든다.
[Symbol.iterator]() {
return this;
},
// ES2015 이전 방식
const a = [27, 14, 12, 64];
console.log(Math.min.apply(Math, a)); // 12
// 스프레드 문법 사용
const a = [27, 14, 12, 64];
console.log(Math.min(...a)); // 12
querySelectorAll
에서 반환된 NodeList
또는 getElementsByTagName
와 기타 이전 메서드에서 반환된 이전 HTMLCollection
같은 다양한 컬렉션 객체가 있다NodeList
는 최신 모던 브라우저(최신 크롬, 파이어폭스, 엣지 및 사파리)에 있으며 HTMLCollection
은 엣지의 크로미움 이전 버전을 제외한 모든 브라우저에도 있다.NodeList
를 HTMLCollection
이 아닌 이터러블로 표시하므로 이전 버전의 엣지는 사양과 일치하는 반면 다른 버전은 지정되지 않았음에도 불구하고 이 기능을 HTMLCollection
에 추가했다.for (const div of document.querySelectorAll('div')) {
// div...
}
[...document.querySelectorAll('div')];
function* simple() {
for (let n = 1; n <= 3; ++n) {
yield n;
}
}
for (const value of simple()) {
console.log(value);
}
for-of
에 제공한다.for-of
는 제너레이터 객체에게 이터레이터를 요청한다. 제너레이터 객체는 자신을 이터레이터로 반환한다.
(제너레이터 객체는 %IteratorPrototype%에서 간접적으로 상속되므로 제공하는 "return this" Symbol.iterator
메서드를 갖는다).for-of
는 제너레이터 객체의 next 메서드를 사용하여 루프에서 값을 가져와 각각 console.log
에 전달한다.function
키워드 뒤에 *
를 주목하자. 이것이 제너레이터 함수를 만든다.yield
에 주목하자. 제너레이터 함수가 겉보기에 일시 중지된 다음 다시 시작되는 위치를 표시한다next
이터레이터 객체를 생성하는 프로토타입과 next
메서드 구현의 모든 코드보다 훨씬 간단하고 간결하다.// 직접 구현
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;
},
};
for (const value of a) {
console.log(value);
}
// 제너레이터 함수 사용
const a = {
0: 'a',
1: 'b',
2: 'c',
length: 3,
// The next example shows a simpler way to write the next line
[Symbol.iterator]: function* () {
for (let index = 0; index < this.length; ++index) {
yield this[index];
}
},
};
for (const value of a) {
console.log(value);
}
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 current = this.head; current; current = current.next) {
yield current.value;
}
}
}
const list = new LinkedList();
list.add('one');
list.add('two');
list.add('three');
for (const value of list) {
console.log(value);
}
function* simple() {
for (let n = 1; n <= 3; ++n) {
yield n;
}
}
const g = simple();
let result;
while (!(result = g.next()).done) {
console.log(result.value);
}
yield
연산자를 통해 값만 제공했다.**yield
연산자의 결과는 제너레이터에 푸시된 값, 즉 소비할 수 있는 값이다.**function* add() {
console.log('starting');
const value1 = yield 'Please provide value 1';
console.log('value1 is ' + value1);
const value2 = yield 'Please provide value 2';
console.log('value2 is ' + value2);
return value1 + value2;
}
let result;
const gen = add();
result = gen.next(); // "starting"
console.log(result); // {value: "Please provide value 1", done: false}
result = gen.next(35); // "value1 is 35"
console.log(result); // {value: "Please provide value 2", done: false}
result = gen.next(7); // "value2 is 7"
console.log(result); // {value: 42, done: true}
const gen add()
는 제너레이터 함수를 호출하고 반환하는 제너레이터 객체를 gen
상수에 저장한다. 제너레이터 함수 내부의 논리가 아직 실행되지 않았다. "시작" 줄은 로그에 나타나지 않는다.gen.next()
는 첫 번째 yield
까지 제너레이터 코드를 실행한다.
"시작" 줄이 출력되고 첫 번째 yield
값인 "Please provide value 1"
이 평가되고 제너레이터가 호출자에게 제공한다.result
에 저장되는 첫 번째 결과 객체의 호출 코드에 의해 수신된다.console.log(result)
는 첫 번째 결과({value: "Please provide value 1", done false)
)를 보여준다.gen.next(35)
는 35 값을 제너레이터로 보낸다.yield
의 결과가 된다.
이것이 전달된 값을 사용 하는 제너레이터다.
코드는 해당 값을 value1
에 할당하고 기록한 다음 "Please provide value 2"
값을 생성(yield
)한다.{value: "Please provide value2", done: false }
)를 기록한다.gen.next(7)
는 7값을 제너레이터로 보낸다.yield
의 결과가 된다.
코드는 이를 value2
에 할당 하고 값을 기록한 다음 value1
과 value2
의 합계를 반환한다.value
에 저장하고 기록한다.
이번에는 value
가 합계(42)이고 done
이 참임을 유의하자. 제너레이터가 그 값을 산출하는 것이 아니라 반환했기 때문에 그렇다.gen.next()
에 대한 첫 번째 호출에는 인수가 없다. 사실 거기에 인수를 넣으면 제너레이터는 그것을 보지 못한다. next
에 대한 첫 번째 호출은 제너레이터를 함수의 시작 부분에서 첫 번째 yield
로 진행한 다음 첫 번째 yield가 생성하는 값을 가진 결과 객체를 반환한다.yield
의 결과로 next
에서 값을 받기 때문에 next
에 대한 첫 번째 호출에서 전달된 값을 받을 방법이 없다. 제너레이터에 초기 입력을 제공하려면 next
에 대한 첫 번째 호출이 아닌 제너레이터 함수의 인수 목록에 인수를 전달하자.next
으로의 첫 번째 호출에 전달하는 대신 add
에 전달한다.function* sumThree() {
const lastThree = [];
let sum = 0;
while (true) {
const value = yield sum;
lastThree.push(value);
sum += value;
if (lastThree.length > 3) {
sum -= lastThree.shift();
}
}
}
const it = sumThree();
console.log(it.next().value); // 0 (there haven't been any values passed in yet)
console.log(it.next(1).value); // 1 (1)
console.log(it.next(7).value); // 8 (1 + 7)
console.log(it.next(4).value); // 12 (1 + 7 + 4)
console.log(it.next(2).value); // 13 (7 + 4 + 2; 1 "fell off")
console.log(it.next(3).value); // 9 (4 + 2 + 3; 7 "fell off")
return
문을 사용할 때 흥미로운 작업을 수행한다.done = true
인 결과 객체를 생성한다.function* usingReturn() {
yield 1;
yield 2;
return 3;
}
console.log('Using for-of:');
for (const value of usingReturn()) {
console.log(value);
}
// =>
// 1
// 2
console.log('Using the generator directly:');
const g = usingReturn();
let result;
while (!(result = g.next()).done) {
console.log(result);
}
// =>
// {value: 1, done: false}
// {value: 2, done: false}
console.log(result);
// =>
// {value: 3, done: true}
yield
연산자는 우선순위가 매우 낮다.yield
가 완료되기 전에 가능한 한 많은 표현식이 함께 그룹화된다.function* example() {
let a = yield +2 + 30; // 잘못됨
return a;
}
const gen = example();
console.log(gen.next()); // {value: 32, done: false}
console.log(gen.next(10)); // {value: 10, done: true}
yield
의 오른쪽에 있는 표현식은 피연산자이며 평가되고 next
값이 된다.let a = yield 2 + 30;
을 수행한다.function* example() {
let x = yield;
let a = x + 2 + 30;
return a;
}
const gen = example();
console.log(gen.next()); // {value: undefined, done: false}
console.log(gen.next(10)); // {value: 42, done: true}
yield
주위에 괄호를 써서 의미 있는 표현식의 어느 곳에서나 사용할 수 있다.function* example() {
let a = (yield) + 2 + 30;
return a;
}
const gen = example();
console.log(gen.next()); // {value: undefined, done: false}
console.log(gen.next(10)); // {value: 42, done: true}
return
과 throw
를 모두 구현한다.return
문을 삽입하거나 현재 yield
가 있는 제너레이터 함수의 내에서 명령문을 throw
할 수 있다.return
메서드를 사용하여 호출 코드는 실제로 제너레이터 함수의 코드에 없는 return
을 발행할 수 있다.function* example() {
yield 1;
yield 2;
yield 3;
}
const gen = example();
console.log(gen.next()); // {value: 1, done: false}
console.log(gen.return(42)); // {value: 42, done: true}
console.log(gen.next()); // {value: undefined, done: true}
throw
메서드는 return
대신 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()); // (never executed)
yield
이 있는 부분에서 new Error("boom");
를 호출한 것과 같다.return
과 throw
문과 마찬가지로 제너레이터는 try/catch/finally
를 사용하여 삽입된 return
과 throw
와 상호 작용할 수 있다.yield*
를 사용하여 해당 제너레이터(또는 이터러블)가 완료되면 다시 실행할 수 있다.yield*
를 수행할 때 throw
와 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); // {value: "outer before", done: false}
result = gen.next();
console.log(result); // {value: "inner 0", done: false}
result = gen.next();
console.log(result); // {value: "inner 1", done: false}
result = gen.return(42); // "inner terminated"
// "outer terminated"
console.log(result); // {value: 42, done: true}
result = gen.next();
console.log(result); // {value: undefined, done: true}
outer
제너레이터가 yield*
를 사용해 inner
제너레이터로 제어를 넘긴다.outer
제너레이터가 return
하는 것이 inner
제너레이터로 포워딩된다.finally
블록에서 console.log
를 통해 확인하자.inner
에서 return
한 것뿐만 아니라 outer
제너레이터에서도 그랬다.inner
제너레이터에서 return 42
가 있는 것과 마찬가지로 outer
제너레이터가 inner
의 반환값을 반환하여 각 제너레이터가 일시 중지되었다.outer
제너레이터는 inner
제너레이터가 종료했다는 이유만으로 값을 생성하는 것을 멈출 필요는 없다.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.return(4)); // { value: "override", done: true }
throw
메서드를 사용하는 것은 이해하기 더 간단하다.for-of
를 사용하자.for (const div of document.querySelectAll('div')) {
// ..
}
Symbol.iterator
함수와 이터레이터를 구현하여 컬렉션 유형을 반복 가능하게 하자.const array = [23, 42, 17, 27];
console.log(Math.min(...array));