시작하기 전에 이 기사가 제가 녹음 한 3 부작 YouTube 시리즈로도 제공된다는 점을 언급하고 싶습니다.
자유롭게 시청하되, 서면 양식을 선호한다면 올바른 위치에 있습니다.
https://mpodlasin.com/articles/iterables-and-iterators
이 글은 자바 스크립트의 Iterables과 Iterators에 대한 심도 있는 소개입니다. 이 글을 쓴 나의 주된 동기는 다음 번에 generators 학습을 준비하는 것이었습니다. 사실, 나중에 제너레이터와 React 후크를 결합하는 것과 같은 실험적인 작업을 할 계획입니다. 흥미롭다면 Twitter 나 YouTube에서 저를 팔로우 하여 향후 기사와 동영상을 확인하세요!
사실, 나는 generators 기사로 시작할 계획이었지만, iterables와 iterators에 대한 확실한 이해 없이는 설명하기 어렵다는 것이 금방 분명해졌습니다.
그렇기 때문에 이 기사에서는 iterables 및 iterators에만 집중할 것입니다. 우리는 그들에 대한 사전 지식이 없다고 가정하지만 동시에 상당히 심층적으로 갈 것입니다. 따라서 iterables 및 iterators에 대해 알고 있지만 여전히 사용하는 것이 편안하지 않다면 이 기사에서 수정해야 합니다.
소개
아시다시피, 우리는 이터러블과 이터레이터에 대해 이야기하고 있습니다. 그것들은 관련이 있지만 뚜렷한 개념이므로 기사를 읽는 동안 주어진 순간에 우리가 이야기하고 있는 것을 확인하십시오.
iterables부터 시작하겠습니다. 그들은 무엇인가? iterable은 기본적으로 다음과 같이 반복 할 수 있는 것입니다.
for (let element of iterable) {
// do something with an element
}
여기서는 ES6에 도입 된 for ... of 루프에 대해서만 이야기하고 있습니다. for ... in 루프는 이전 구조이므로 이 기사에서는 전혀 사용하지 않습니다.
이제 "좋아,이 반복 가능한 변수는 단순히 배열입니다!"라고 생각할 수 있습니다. 그리고 실제로 배열은 반복 가능합니다. 그러나 현재 네이티브 JavaScript에서도 for ... of 루프에서 사용할 수 있는 다른 데이터 구조가 있습니다. 즉, 네이티브 JavaScript에는 배열보다 더 많은 이터러블이 있습니다.
예를 들어 ES6 Maps를 반복 할 수 있습니다.
const ourMap = new Map();
ourMap.set(1, 'a');
ourMap.set(2, 'b');
ourMap.set(3, 'c');
for (let element of ourMap) {
console.log(element);
}
이 코드는 다음을 인쇄합니다.
[1, 'a']
[2, 'b']
[3, 'c']
따라서 위 코드의 변수 요소는 각 반복 단계에서 두 요소의 배열을 저장합니다. 첫 번째 요소는 키이고 두 번째 요소는 값입니다.
for ... of loop를 사용하여 Map을 반복 할 수 있다는 사실은 Maps가 반복 가능하다는 것을 증명합니다. 다시 한번-for ... of loops에서만 iterable을 사용할 수 있습니다. 따라서 해당 루프와 함께 작동하는 것이 있으면 반복 가능합니다.
재미있게도 Map 생성자 자체는 선택적으로 키-값 쌍의 반복 가능을 허용합니다. 따라서 이것은 이전과 동일한 맵을 구성하는 다른 방법입니다.
const ourMap = new Map([
[1, 'a'],
[2, 'b'],
[3, 'c'],
]);
그리고 방금 언급했듯이 Map 자체가 반복 가능하므로 Map 사본을 매우 쉽게 만들 수 있습니다.
const copyOfOurMap = new Map(ourMap);
동일한 키에 동일한 값을 저장하고 있지만 이제 두 개의 별개의 맵이 있습니다.
그래서 우리는 지금까지 이터러블의 두 가지 예를 보았습니다. 배열과 ES6 맵입니다.
그러나 우리는 그들이 반복 될 수 있는 이 마법의 힘을 어떻게 소유하고 있는지 설명하지 않았습니다.
대답은 간단합니다-그들과 관련된 반복자가 있습니다. 주의 깊게 읽으십시오. 반복자가 아닌 반복자.
이터레이터는 어떤 방식으로 이터러블과 연관됩니까? 반복 가능한 객체는 Symbol.iterator 속성 아래에 함수가 있으면 됩니다. 이 함수는 호출 될 때 해당 객체에 대한 반복자를 반환해야 합니다.
예를 들어 다음과 같이 배열의 반복자를 검색 할 수 있습니다.
const ourArray = [1, 2, 3];
const iterator = ourArray[Symbol.iterator]();
console.log(iterator);
이 코드는 Object [Array Iterator]{}를 콘솔에 출력합니다.
그래서 우리는 배열에 연관된 반복기가 있고 이 반복기가 일종의 객체라는 것을 압니다.
그렇다면 반복자는 무엇입니까?
매우 간단합니다. 반복자는 다음 메서드를 가진 객체 일 뿐입니다. 이 메서드는 호출 될 때 다음을 반환해야 합니다.
배열 반복기의 다음 메서드를 호출하여 테스트 해 보겠습니다.
const result = iterator.next();
console.log(result);
콘솔에 {value : 1, done : false} 객체가 인쇄되는 것을 볼 수 있습니다.
우리가 만든 배열의 첫 번째 요소는 1이므로 여기에 값으로 나타납니다. 또한 반복기가 아직 완료되지 않았다는 정보도 얻었습니다. 이는 여전히 다음 함수를 호출하고 일부 값을 볼 수 있음을 의미합니다.
해보자! 실제로 다음 두 번 더 전화 해 보겠습니다.
console.log(iterator.next());
console.log(iterator.next());
당연히 {value : 2, done : false} 및 {value : 3, done : false}가 차례로 인쇄됩니다.
그러나 우리 배열에는 3 개의 요소만 있었습니다. 그럼 다음 번에 다시 호출을 하면 어떻게 될까요?
console.log(iterator.next());
이번에는 {value : undefined, done : true}가 인쇄 된 것을 볼 수 있습니다. 이터레이터가 완료되었다는 정보입니다. 다시 다음을 부르는 요점이 없습니다. 실제로 그렇게 하면 동일한 {value : undefined, done : true} 객체를 반복해서 받게 됩니다. done : true는 반복을 중지하라는 신호입니다.
이제 우리는 for ... of 루프가 내부적으로 무엇을 하는지 이해할 수 있습니다.
모든 것을 코드로 작성해 보겠습니다.
const iterator = ourArray[Symbol.iterator]();
let result = iterator.next();
while (!result.done) {
const element = result.value;
// do some something with element
result = iterator.next();
}
이 모든 코드는 다음과 직접적으로 동일합니다.
for (let element of ourArray) {
// do something with element
}
예를 들어, // 요소 주석으로 무언가를 수행하는 대신 console.log (element)를 배치하여 이러한 사실을 확인할 수 있습니다.
자체 반복기 만들기
그래서 우리는 이터러블과 이터레이터가 무엇인지 압니다. 그러면 질문은-우리 자신의 인스턴스를 작성할 수 있습니까?
물론!
반복자에는 마법 같은 것이 없습니다. 지정된 방식으로 작동하는 다음 메소드가 있는 객체일 뿐입니다.
우리는 어떤 네이티브 JS 값이 반복 가능한지 언급했습니다. 우리는 거기에 물건을 언급하지 않았습니다. 실제로, 기본적으로 반복 가능하지 않습니다. 다음과 같은 개체를 가져옵니다.
const ourObject = {
1: 'a',
2: 'b',
3: 'c'
};
for (ourObject의 let 요소)를 사용하여 해당 객체를 반복하려고 하면 해당 객체가 반복 할 수 없다는 오류가 발생합니다.
따라서 이러한 객체를 반복 가능하게 만들어 사용자 지정 반복자를 작성하는 연습을 해봅시다!
이를 위해서는 사용자 정의 [Symbol.iterator]() 메서드로 Object 프로토 타입을 패치해야 합니다. 프로토 타입을 패치 하는 것은 나쁜 습관이므로 Object를 확장하여 사용자 정의 클래스를 생성 해 보겠습니다.
class IterableObject extends Object {
constructor(object) {
super();
Object.assign(this, object);
}
}
우리 클래스의 생성자는 단순히 일반 객체를 취하고 그 속성을 반복 가능한 객체에 복사합니다 (아직 실제로 반복 할 수는 없지만!).
그래서 우리는 다음과 같은 상호 작용 가능한 객체를 만들 것입니다 :
const iterableObject = new IterableObject({
1: 'a',
2: 'b',
3: 'c'
})
IterableObject 클래스를 실제로 반복 가능하게 하려면 [Symbol.iterator]() 메서드가 있어야 합니다. 그럼 추가합시다.
class IterableObject extends Object {
constructor(object) {
super();
Object.assign(this, object);
}
[Symbol.iterator]() {
}
}
이제 실제 반복기 작성을 시작할 수 있습니다!
우리는 이미 다음 메소드가 있는 객체여야 한다는 것을 알고 있습니다. 그래서 그것부터 시작합시다.
class IterableObject extends Object {
// same as before
[Symbol.iterator]() {
return {
next() {}
}
}
}
next를 호출 할 때마다 {value, done} 모양의 객체를 반환해야 합니다. 더미 값을 사용하여 그렇게 합시다.
class IterableObject extends Object {
// same as before
[Symbol.iterator]() {
return {
next() {
return {
value: undefined,
done: false
}
}
}
}
}
반복 가능한 객체가 주어지면 :
const iterableObject = new IterableObject({
1: 'a',
2: 'b',
3: 'c'
})
ES6 Map에서 반복 한 것과 유사하게 키-값 쌍을 인쇄하고 싶습니다.
['1', 'a']
['2', 'b']
['3', 'c']
따라서 사용자 지정 반복기에서 value 속성 아래에 [key, valueForThatKey] 배열을 배치하려고 합니다.
이는 이전 단계의 예와 비교하여 자체 설계 결정입니다. 키만 반환하거나 속성 값만 반환하는 이터레이터를 작성하려는 경우에도 그렇게 할 수 있으며 완벽하게 괜찮을 것입니다. 우리는 단순히 키-값 쌍을 반환하기로 결정했습니다.
따라서 [key, valueForThatKey] 모양의 배열이 필요합니다. 이를 얻는 가장 쉬운 방법은 Object.entries 메소드를 사용하는 것입니다.
[Symbol.iterator] () 메서드에서 반복기 객체를 만들기 직전에 사용할 수 있습니다.
class IterableObject extends Object {
// same as before
[Symbol.iterator]() {
// we made an addition here
const entries = Object.entries(this);
return {
next() {
return {
value: undefined,
done: false
}
}
}
}
}
해당 메서드에서 반환 된 반복자는 JavaScript 클로저 덕분에 항목 변수에 액세스 할 수 있습니다.
그러나 우리는 또한 어떤 종류의 상태 변수가 필요합니다. 현재 다음 호출에서 반환되어야 하는 키-값 쌍을 알려줍니다. 그래서 그것도 추가합시다.
class IterableObject extends Object {
// same as before
[Symbol.iterator]() {
const entries = Object.entries(this);
// we made an addition here
let index = 0;
return {
next() {
return {
value: undefined,
done: false
}
}
}
}
}
다음 호출마다 값을 업데이트 할 계획이라는 것을 알고 있으므로 let을 사용하여 인덱스 변수를 선언 한 방법에 유의하십시오.
이제 다음 메서드에서 실제 값을 반환 할 준비가 되었습니다.
class IterableObject extends Object {
// same as before
[Symbol.iterator]() {
const entries = Object.entries(this);
let index = 0;
return {
next() {
return {
// we made a change here
value: entries[index],
done: false
}
}
}
}
}
이것은 쉬웠다. 항목 배열에서 적절한 키-값 쌍에 액세스하기 위해 항목과 색인 변수를 모두 사용했습니다.
이제 done 속성을 처리해야 합니다. 현재는 항상 false로 설정되기 때문입니다.
항목 및 색인과 함께 다른 변수를 유지하고 다음 호출마다 업데이트 할 수 있습니다. 그러나 더 쉬운 방법이 있습니다. 인덱스가 이미 항목 배열의 범위를 벗어 났는지 간단히 확인할 수 있습니다.
class IterableObject extends Object {
// same as before
[Symbol.iterator]() {
const entries = Object.entries(this);
let index = 0;
return {
next() {
return {
value: entries[index],
// we made a change here
done: index >= entries.length
}
}
}
}
}
실제로 우리의 반복자는 인덱스 변수가 항목의 길이와 같거나 더 클 때 수행됩니다.
예를 들어 항목의 길이가 3이면 인덱스 0, 1 및 2 아래에 값이 있습니다. 따라서 인덱스 변수가 3 (길이와 같음) 이상이면 가져올 값이 더 이상 없음을 의미합니다. 그 때 우리가 끝났습니다.
이 코드는 거의 작동합니다. 추가해야 할 것이 하나 더 있습니다.
인덱스 변수는 값 0으로 시작하지만, 우리는 절대 업데이트하지 않습니다!
{value, done}을 반환 한 후에 업데이트 해야 하므로 실제로 다소 까다롭습니다. 그러나 그것을 반환하면 return 문 뒤에 코드가 있더라도 다음 메서드가 즉시 실행을 중지합니다.
그러나 {value, done} 객체를 만들고 변수에 저장하고 인덱스를 업데이트 한 다음 객체를 반환 할 수 있습니다.
class IterableObject extends Object {
// same as before
[Symbol.iterator]() {
const entries = Object.entries(this);
let index = 0;
return {
next() {
const result = {
value: entries[index],
done: index >= entries.length
};
index++;
return result;
}
}
}
}
이러한 모든 변경 후, 지금까지 IterableObject 클래스의 모습은 다음과 같습니다.
class IterableObject extends Object {
constructor(object) {
super();
Object.assign(this, object);
}
[Symbol.iterator]() {
const entries = Object.entries(this);
let index = 0;
return {
next() {
const result = {
value: entries[index],
done: index >= entries.length
};
index++;
return result;
}
}
}
}
이 코드는 완벽하게 작동하지만 약간 복잡해졌습니다. 결과 객체를 생성 한 후 인덱스를 업데이트해야 하는 문제를 처리하는 더 현명한 (그러나 덜 분명한) 방법이 있습니다. -1로 간단히 인덱스를 초기화 할 수 있습니다!
그런 다음 인덱스 업데이트가 다음 개체를 반환하기 전에 발생하더라도 첫 번째 업데이트가 -1에서 0으로 증가하기 때문에 모든 것이 정상적으로 작동합니다.
그래서 그렇게 합시다 :
class IterableObject extends Object {
// same as before
[Symbol.iterator]() {
const entries = Object.entries(this);
let index = -1;
return {
next() {
index++;
return {
value: entries[index],
done: index >= entries.length
}
}
}
}
}
보시다시피, 이제 결과 개체를 만들고 인덱스를 업데이트하는 순서를 조정할 필요가 없습니다. -1로 시작하기 때문입니다. 첫 번째 다음 호출 중에 인덱스가 0으로 업데이트 된 다음 결과를 반환합니다.
두 번째 호출 중에 인덱스가 1로 업데이트 되고 다른 결과 등이 반환 됩니다.
따라서 모든 것이 우리가 원하는 대로 작동하고 코드는 이전 버전보다 훨씬 간단 해 보입니다.
제대로 작동하는지 어떻게 테스트 할 수 있습니까? 수동으로 [Symbol.iterator] () 메서드를 실행하여 반복기 인스턴스를 만든 다음 다음 호출의 결과 등을 직접 테스트 할 수 있습니다.
그러나 훨씬 더 간단한 방법이 있습니다! 우리는 모든 iterable을 for ... of loop에 연결할 수 있다고 말했습니다! 그래서 그렇게 하고 커스텀 iterable이 반환 한 값을 기록해 봅시다 :
const iterableObject = new IterableObject({
1: 'a',
2: 'b',
3: 'c'
});
for (let element of iterableObject) {
console.log(element);
}
효과가 있다! 콘솔에 다음 결과가 인쇄됩니다.
[ '1', 'a' ]
[ '2', 'b' ]
[ '3', 'c' ]
그것이 바로 우리가 원했던 것입니다!
멋지지 않나요? 기본적으로 반복기가 내장되어 있지 않기 때문에 for ... of 루프에서 사용할 수 없는 객체로 시작했습니다. 그러나 우리는 수작업으로 작성한 연관된 이터레이터가 있는 커스텀 IterableObject를 생성했습니다.
이제 이터 러블과 이터레이터의 힘을 보고 감사 할 수 있기를 바랍니다. 이는 고유 데이터 구조와 구별 할 수 없는 방식으로 for ... of loops와 같은 JS 기능과 협력 할 수 있는 자체 데이터 구조를 허용하는 메커니즘입니다! 이는 매우 강력하며 특정 상황에서 특히 데이터 구조에 대해 반복을 자주 수행하려는 경우 코드를 크게 단순화 할 수 있습니다.
또한 이러한 반복이 정확히 무엇을 반환할지 사용자 지정할 수 있습니다. 반복기에서 키-값 쌍을 반환하기로 결정했습니다. 그러나 우리가 가치 자체에만 관심이 있다면 어떨까요? 문제 없어요! 반복자를 다시 작성할 수 있습니다.
class IterableObject extends Object {
// same as before
[Symbol.iterator]() {
// changed `entries` to `values`
const values = Object.values(this);
let index = -1;
return {
next() {
index++;
return {
// changed `entries` to `values`
value: values[index],
// changed `entries` to `values`
done: index >= values.length
}
}
}
}
}
그리고 그게 다야!
이 변경 후 for ... of 루프를 실행하면 콘솔에 다음 출력이 표시됩니다.
a
b
c
그래서 우리는 우리가 원하는 대로 객체 값만 반환했습니다.
이것은 사용자 지정 반복기가 얼마나 유연 할 수 있는지 증명합니다. 당신이 원하는 것은 무엇이든 반환하도록 할 수 있습니다.
Iterators as ... iterables
사람들이 이터레이터와 이터러블을 매우 자주 혼동하는 것을 볼 수 있습니다.
그것은 실수이며 이 기사에서 두 가지를 신중하게 구별하려고 노력했지만 사람들이 자주 혼동하는 주된 이유 중 하나를 알고 있다고 생각합니다.
이터레이터는 ... 때로는 이터러블이기도합니다!
무슨 뜻이에요? 이터러블은 연관된 이터레이터가 있는 객체라고 말했습니다.
모든 네이티브 자바 스크립트 이터레이터에는 [Symbol.iterator] () 메서드가 있어 또 다른 이터레이터를 반환합니다! 이것은-우리의 이전 정의에 따르면-첫 번째 반복자를 반복 가능하게 만듭니다.
배열에서 반환 된 반복기를 가져 와서 [Symbol.iterator] ()를 한 번 더 호출하여 이것이 참인지 확인할 수 있습니다.
const ourArray = [1, 2, 3];
const iterator = ourArray[Symbol.iterator]();
const secondIterator = iterator[Symbol.iterator]();
console.log(secondIterator);
이 코드를 실행하면 Object [Array Iterator] {}가 표시됩니다.
따라서 우리의 이터레이터에는 또 다른 이터레이터가 연결되어있을뿐만 아니라 다시 배열 이터레이터라는 것을 알 수 있습니다.
사실,이 두 반복자를 ===와 비교해 보면 이것이 정확히 동일한 반복 자라는 것이 밝혀졌습니다.
const iterator = ourArray[Symbol.iterator]();
const secondIterator = iterator[Symbol.iterator]();
// logs `true`
console.log(iterator === secondIterator);
반복자가 자체 반복자 인이 동작은 처음에는 이상하게 보일 수 있습니다.
그러나 실제로는 상당히 유용합니다.
베어 반복기를 for ... of 루프에 연결할 수 없습니다. for ... of는 [Symbol.iterator] () 메서드가 있는 객체 인 이터 러블 만 허용합니다.
그러나 반복자가 자체 반복자 (따라서 반복 가능) 인 경우 해당 문제가 완화됩니다. 네이티브 JavaScript 반복기에는 [Symbol.iterator] () 메서드가 있으므로 두 번 생각하지 않고 for ... of 루프에 직접 전달할 수 있습니다.
따라서 이 기능으로 인해 두 가지 모두 :
const ourArray = [1, 2, 3];
for (let element of ourArray) {
console.log(element);
}
과:
const ourArray = [1, 2, 3];
const iterator = ourArray[Symbol.iterator]();
for (let element of iterator) {
console.log(element);
}
문제 없이 일하고 똑같은 일을 합니다.
하지만 for ... of 루프에서 직접 반복자를 사용하려는 이유는 무엇입니까? 대답은 간단합니다. 때로는 피할 수 없는 경우도 있습니다.
우선, 그것이 속한 이터 러블없이 이터레이터를 만들고 싶을 수 있습니다. 우리는 나중에 그러한 예를 보게 될 것이며 실제로 그러한 "베어"반복자를 만드는 것은 그리 드문 일이 아닙니다. 때때로 iterable 자체는 필요하지 않습니다.
베어 이터레이터가 있다는 의미에서 for ... of를 통해 소비 할 수 없다는 것은 매우 비판적 일 것입니다. 물론 다음 메서드와 예를 들어 while 루프를 사용하여 수동으로 수행하는 것은 물론 가능하지만 많은 타이핑과 상용구가 필요하다는 것을 알았습니다.
간단합니다. 상용구를 피하고 for ... of 루프에서 반복자를 사용하려면 반복 가능하게 만들어야 합니다.
반면에 [Symbol.iterator] () 이외의 메서드에서 반복기를 받는 경우도 매우 많습니다. 예를 들어 ES6 맵에는 항목, 값 및 키 방법이 있습니다. 모두 반복자를 반환합니다.
네이티브 JavaScript 반복기가 반복 가능하지 않은 경우 다음과 같은 for ... 루프에서 이러한 메서드를 직접 사용할 수 없습니다.
for (let element of map.entries()) {
console.log(element);
}
for (let element of map.values()) {
console.log(element);
}
for (let element of map.keys()) {
console.log(element);
}
메서드에 의해 반환 된 반복기도 반복 가능하기 때문에 위의 코드가 작동합니다.
그렇지 않은 경우, 예를 들어 map.entries () 호출의 결과를 일종의 더미 이터 러블에 어색하게 래핑해야 합니다. 운 좋게도 우리는 그럴 필요가 없으며, 그 방법에 대해 너무 걱정하지 않고 직접 사용할 수 있습니다.
이러한 이유로 사용자 지정 반복자도 반복 가능하게 만드는 것이 좋습니다. 특히 [Symbol.iterator] () 이외의 메서드에서 반환 되는 경우.
그리고 실제로 반복자를 반복 가능하게 만드는 것은 매우 간단합니다. IterableObject 이터레이터를 사용해 봅시다.
class IterableObject extends Object {
// same as before
[Symbol.iterator]() {
// same as before
return {
next() {
// same as before
},
[Symbol.iterator]() {
return this;
}
}
}
}
보시다시피 다음 메소드 아래에 [Symbol.iterator] () 메소드를 생성했습니다.
우리는 단순히 이것을 반환함으로써 이 반복기를 자체 반복자로 만들었습니다. 우리는 배열 반복기가 정확히 어떻게 동작하는지 보았습니다.
반복자가 for ... of 루프와 함께 작동하는지 확인하기에 충분합니다.
반복기의 상태
이제 각 이터레이터가 그와 관련된 상태를 가지고 있다는 것은 분명합니다.
예를 들어 IterableObject 반복기에서 인덱스 변수 인 상태를 클로저로 유지했습니다.
각 반복 단계 후에 해당 색인이 업데이트 되었습니다.
그렇다면 반복 프로세스가 끝나면 어떻게 될까요? 그것은 간단합니다-반복자는 쓸모 없게 되고 우리는 그것을 버릴 수 있습니다.
네이티브 자바 스크립트 객체의 반복자에서도 이런 일이 발생하는지 다시 확인할 수 있습니다.
배열의 반복자를 가져와 for ... of 루프에서 두 번 실행하려고 합니다.
const ourArray = [1, 2, 3];
const iterator = ourArray[Symbol.iterator]();
for (let element of iterator) {
console.log(element);
}
for (let element of iterator) {
console.log(element);
}
콘솔에 숫자 1, 2, 3이 두 번 나타날 것으로 예상 할 수 있습니다. 그러나 이것은 일어나는 일이 아닙니다. 결과는 여전히 다음과 같습니다.
1
2
3
그런데 왜?
루프가 완료된 후 next를 수동으로 호출하여 다음을 발견 할 수 있습니다.
const ourArray = [1, 2, 3];
const iterator = ourArray[Symbol.iterator]();
for (let element of iterator) {
console.log(element);
}
console.log(iterator.next());
마지막 로그는 콘솔에 {value : undefined, done : true}를 인쇄합니다.
아아. 따라서 루프가 완료된 후 반복기는 이제 "완료"상태가 됩니다. 이제부터는 항상 {value : undefined, done : true} 객체를 반환합니다.
for ... of 루프에서 두 번째로 사용하기 위해 이 반복기의 상태를 "재설정"하는 방법이 있습니까?
어떤 경우에는 아마도 그러나 실제로는 의미가 없습니다. 이것이 바로 [Symbol.iterator]가 단순한 속성이 아닌 메서드인 이유입니다. 이 메서드를 다시 호출하여 다른 반복자를 얻을 수 있습니다.
const ourArray = [1, 2, 3];
const iterator = ourArray[Symbol.iterator]();
for (let element of iterator) {
console.log(element);
}
const secondIterator = ourArray[Symbol.iterator]();
for (let element of secondIterator) {
console.log(element);
}
이제 예상대로 작동합니다.
지금 바로 배열을 직접 여러 번 반복하는 것이 작동하는 이유를 이해할 수 있어야 합니다.
const ourArray = [1, 2, 3];
for (let element of ourArray) {
console.log(element);
}
for (let element of ourArray) {
console.log(element);
}
for ... of 루프가 각각 다른 반복자를 사용하기 때문입니다! 반복기가 완료되고 루프가 종료 된 후에는 해당 반복기가 다시 사용되지 않습니다.
반복자 대 배열
for ... of 루프에서 (간접적이긴 하지만) 반복자를 사용하기 때문에, 그들은 배열과 유사하게 보일 수 있습니다.
그러나 반복기와 배열 사이에는 두 가지 중요한 차이점이 있습니다.
둘 다 열망과 게으른 가치의 개념과 관련이 있습니다.
배열을 만들 때 주어진 순간에 특정 길이를 가지며 해당 값은 이미 초기화되어 있습니다.
내 말은, 안에 어떤 값도 없이 배열을 만들 수 있다는 뜻입니다.하지만 여기서 의미하는 바는 아닙니다.
array [someIndex]를 써서 그 값에 접근을 시도한 후에야 그 값을 초기화하는 배열을 생성 할 수 없다는 것을 의미합니다. 내 말은, 아마도 일부 프록시 또는 다른 JS 속임수로 가능하지만 기본적으로 JavaScript 배열은 그런 방식으로 작동하지 않습니다. 미리 초기화 된 값으로 배열을 생성하기 만하면 됩니다.
그리고 배열에 길이가 있다고 말할 때 실제로 배열의 길이가 유한하다는 것을 의미합니다. JavaScript에는 무한 배열이 없습니다.
이 두 가지 특성은 배열에 대한 열의를 나타냅니다.
반면에 반복자는 게으르다.
이를 보여주기 위해 두 개의 사용자 지정 반복기를 만들 것입니다. 첫 번째는 유한 배열과 달리 무한 반복기가 되고, 두 번째는 반복기를 사용하는 사람이 실제로 필요 / 요청할 때만 값을 초기화합니다.
무한 반복자부터 시작하겠습니다. 이것은 무섭게 들릴지 모르지만 매우 간단한 것을 만들 것입니다. 0에서 시작하고 각 단계에서 시퀀스의 다음 정수를 반환하는 반복기입니다. 영원히.
const counterIterator = {
integer: -1,
next() {
this.integer++;
return { value: this.integer, done: false };
},
[Symbol.iterator]() {
return this;
}
}
그게 다야! -1과 같은 정수 속성으로 시작합니다. 다음에 호출 할 때마다 하나씩 범프하고 결과 객체의 값으로 반환합니다.
여기서는 첫 번째 결과로 0을 반환하기 위해 -1에서 시작하는 이전과 동일한 트릭을 사용했습니다.
done 속성도 보세요. 항상 거짓입니다. 이 반복자는 끝나지 않습니다!
세 번째로, 아마 여러분 스스로 눈치 채 셨을 것입니다. 간단한 [Symbol.iterator] () 구현을 제공하여 이 반복자를 반복 가능하게 만들었습니다.
그리고 마지막 메모입니다. 이것은 우리가 이전에 언급 한 경우입니다. 우리는 반복자를 만들었지 만 반복 할 수 있는 것이 보이지 않습니다! 이것은 반복 가능한 "부모"가 필요 없는 반복자입니다.
이제 for ... of 루프에서 이 반복기를 사용해 볼 수 있습니다. 우리는 어떤 시점에서 루프에서 벗어나는 것을 기억하면 됩니다. 그렇지 않으면 코드가 영원히 실행됩니다!
for (let element of counterIterator) {
if (element > 5) {
break;
}
console.log(element);
}
이 코드를 실행하면 콘솔에 다음이 표시됩니다.
0
1
2
3
4
5
그래서 우리는 원하는 만큼의 정수를 반환 할 수 있는 무한 반복기를 만들었습니다. 그리고 실제로 달성하는 것은 매우 쉬웠습니다!
이제 요청 될 때까지 값을 생성하지 않는 반복자를 만들어 보겠습니다.
음 ... 우리는 이미 해냈습니다!
주어진 순간에 우리 counterIterator는 정수 속성에 하나의 숫자 만 저장한다는 것을 알고 있습니까? 다음 호출에서 반환 된 마지막 번호 만 저장합니다.
이것은 실제로 우리가 이야기했던 게으름입니다. 이 반복기는 잠재적으로 모든 숫자 (구체적으로는 음이 아닌 정수)를 반환 할 수 있습니다. 그러나 누군가가 다음 메서드를 호출 할 때 실제로 필요할 때만 번호를 생성합니다.
이것은 큰 이점이 아닌 것 같습니다. 결국 숫자는 빠르게 생성되고 많은 메모리를 차지하지 않습니다.
그러나 코드에서 매우 크고 메모리가 많은 개체를 처리하는 경우 때때로 배열을 반복자로 교체하는 것이 매우 중요 할 수 있으므로 프로그램의 메모리 효율성을 높일 수 있습니다.
객체가 무거울수록 (또는 생성하는 데 시간이 오래 걸릴수록) 이점이 커집니다.
iterables를 소비하는 다른 방법
지금까지 for ... of 루프로만 플레이했거나 다음 메서드를 사용하여 반복기를 수동으로 소비했습니다.
그러나 이것이 유일한 선택은 아닙니다!
우리는 이미 Map 생성자가 iterable을 인수로 받아들이는 것을 보았습니다.
Array.from 메서드를 사용하여 iterable을 실제 배열로 쉽게 변환 할 수도 있습니다. 그래도 조심하세요! 우리가 말했듯이 게으름은 때때로 반복자의 큰 이점입니다. 그것을 배열로 변환하면 모든 게으름이 제거됩니다. 반복기에 의해 반환 된 모든 값은 즉시 초기화 된 다음 배열에 저장됩니다.
특히 이것은 무한한 counterIterator를 배열로 변환하려고 시도하면 재앙을 초래할 수 있음을 의미합니다. Array.from은 영원히 실행되며 결과를 반환하지 않습니다! 따라서 반복 가능 / 반복자를 배열로 변환하기 전에 안전한 작업인지 확인하십시오.
흥미롭게도 iterable은 스프레드 연산자 (...) 와도 잘 작동합니다. 반복자의 모든 값이 한 번에 초기화되는 Array.from과 유사하게 작동한다는 점을 명심하십시오.
예를 들어 spread 연산자를 사용하여 자체 버전의 Array.from을 만들 수 있습니다.
이터러블에 연산자를 적용한 다음 값을 배열에 넣습니다.
const arrayFromIterator = [...iterable];
iterable에서 모든 값을 가져 와서 함수에 적용 할 수도 있습니다.
someFunction(...iterable);
결론
이 시점에서 이 기사의 제목이 "Iterables and Iterators"인 이유를 이해하시기 바랍니다.
우리는 그것들이 무엇인지, 어떻게 다른지, 어떻게 사용하는지, 그리고 그들 자신의 인스턴스를 만드는 방법을 배웠습니다.
이것은 우리가 발전기를 다룰 준비가 된 것 이상을 만듭니다. 사실 반복자를 잘 이해하고 있다면 제너레이터로 뛰어 드는 것은 전혀 문제가 되지 않습니다!
등록된 댓글이 없습니다.