일반적으로 사용하는 리스트 순회는 다음과 같다.
const list = [1, 2, 3];
for( var i = 0; i< list.length; i++){
log(list[i]);
}
/*유사 배열 순회*/
const str = 'abc';
for( var i = 0; i< str.length; i++){
log(str[i]);
}
하지만, ES6에서는 for of, for in 문법이 나타나며 이를 이용하여 리스트 순회를 할 수 있게 되었다.
✏️ for of: 주로 배열반복에 사용
const list = [1, 2, 3];
for (const a of list){
console.log(a);
}
✏️ for in: 주로 객체반복에 사용
const AA = [1, 2, 3];
const BB = {'a':1, 'b':2, 'c': 3};
for (const a in AA){
console.log(a);
}
for (const a in BB){
console.log(a);
}
✅ 기존의 ES5에서 사용되는 순회는 length라는 속성에 의존한다. 즉, 순회하고자 하면 꼭 proto 또는 constructor에서 length를 프로토타입 기반으로 상속받아야 한다는 것이다.
ES6에서는 이터레이터가 등장하면서 더 이상 length에 의재 의존하지 않고 이터레이터를 이용한 순환이 가능해진 것이다.
앞서 본 for of, for in문 역시 이터러블 객체의 이터레이터를 활용하여 객체를 순환한다. for of문을 사용하면 이터러블 객체의 이터레이터를 받아와서 next()메서드를 통해 순회하여 복잡한 과정을 거치지 않아도 되는 것이다.
👉 Iteration: 반복
👉 규약: " 이터레이션 할 수 있는 구조가 되어야 한다. "
ES6에서 도입된 이터레이션 프로토콜(iteration protocol)은 데이터를 순회하기 위한 프로토콜(미리 약속된 규칙)이다.
이터레이션 프로토콜에는 Iterable Protocol 과 Iterator Protocol이 있다.
✏️ 이터러블(iterable)프로토콜
✏️ 이터레이터(iterator)프로토콜
✔️ 단, 앞서 말했듯이 프로토콜을 준수하면 이터레이션이 가능하다. 즉, 이터레이션 할 수 없는 오브젝트를 이터레이션 할 수 있도록 커스텀(사용자 정의) 이터러블을 만들어 낼 수 있다.(커스텀 이터러블은 아래에서!)
💡 Why? 이터레이션 프로토콜이 왜 필요한건데?
데이터 소비자(Data consumer)인 for…of 문, spread 문법 등은 여러 빌트인 오브젝트들을 가지고 있다.(Array, String, Map...)
이런 빌트인 오브젝트인 데이터 소스들은 모두 이터레이션 프로토콜을 준수하는 이터러블이다. 즉, 이터러블은 데이터 공급자(Data provider)의 역할을 한다.만약 이처럼 다양한 데이터 소스가 각자의 순회 방식을 갖는다면 데이터 소비자는 다양한 데이터 소스의 순회 방식을 모두 지원해야 한다. 이는 효율적이지 않다. 하지만 다양한 데이터 소스가 이터레이션 프로토콜을 준수하도록 규정하면 데이터 소비자는 이터레이션 프로토콜만을 지원하도록 구현하면 된다.
즉, 이터레이션 프로토콜은 다양한 데이터 소스가 하나의 순회 방식을 갖도록 규정하여 데이터 소비자가 효율적으로 다양한 데이터 소스를 사용할 수 있도록 데이터 소비자와 데이터 소스를 연결하는 인터페이스의 역할을 한다.
이터러블을 지원하는 데이터 소비자는 내부에서 Symbol.iterator 메소드를 호출해 이터레이터를 생성하고 이터레리터의 next 메소드를 호출하여 이터러블을 순회한다. 그리고 next 메소드가 반환한 이터레이터 리절트 객체의 value 프로퍼티 값을 취득한다.
이터러블 프로토콜은 오브젝트가 반복 가능한 구조이어야 하고, Symbol.iterator을 가지고 있어야 하는 것이다. 이 규약을 준수한 객체를 이터러블이라 한다.
👉 즉, 이터러블은 객체의 값을 반복 순회할 수 있는 자격을 가진 객체이다.
Symbol.iterator 메소드는 이터레이터를 반환한다. 이터러블은 for…of 문에서 순회할 수 있으며 Spread 문법의 대상으로 사용할 수 있다.
✏️ 다음의 빌트인 오브젝트는 디폴트로 이터러블 프로토콜이 있다. 즉, Symbol.iterator을 가지고 있다.
// 배열은 이터러블이다.
const array = [1, 2, 3];
// 이터러블은 Symbol.iterator 메소드를 소유한다.
// Symbol.iterator 메소드는 이터레이터를 반환한다.
let iter = array[Symbol.iterator]();
// 이터레이터는 next 메소드를 소유한다.
// next 메소드는 이터레이터 리절트 객체를 반환한다.
console.log(iter.next()); // {value: 1, done: false}
console.log(iter.next()); // {value: 2, done: false}
console.log(iter.next()); // {value: 3, done: false}
console.log(iter.next()); // {value: undefined, done: true}
// 이터러블은 for...of 문으로 순회 가능하다.
for (const item of array) {
console.log(item);
}
// 문자열은 이터러블이다.
const string = 'hi';
// 이터러블은 Symbol.iterator 메소드를 소유한다.
// Symbol.iterator 메소드는 이터레이터를 반환한다.
iter = string[Symbol.iterator]();
// 이터레이터는 next 메소드를 소유한다.
// next 메소드는 이터레이터 리절트 객체를 반환한다.
console.log(iter.next()); // {value: "h", done: false}
console.log(iter.next()); // {value: "i", done: false}
console.log(iter.next()); // {value: undefined, done: true}
// 이터러블은 for...of 문으로 순회 가능하다.
for (const letter of string) {
console.log(letter);
}
// arguments 객체는 이터러블이다.
(function () {
// 이터러블은 Symbol.iterator 메소드를 소유한다.
// Symbol.iterator 메소드는 이터레이터를 반환한다.
iter = arguments[Symbol.iterator]();
// 이터레이터는 next 메소드를 소유한다.
// next 메소드는 이터레이터 리절트 객체를 반환한다.
console.log(iter.next()); // {value: 1, done: false}
console.log(iter.next()); // {value: 2, done: false}
console.log(iter.next()); // {value: undefined, done: true}
// 이터러블은 for...of 문으로 순회 가능하다.
for (const arg of arguments) {
console.log(arg);
}
}(1, 2));
const arr = [1, 2, 3];
for(const a of arr) log(a)
//결과
1
2
3
💡 아래와 같이 배열의 Key로 접근하여 요소를 순회 가능
const arr = [1, 2, 3];
console.log(arr[0]); // 1
console.log(arr[2]); // 3
const set = new Set([1,2,3]);
for(const a of set) log(a);
//결과
1
2
3
💡 Set 자료형의 경우는 Arr 와 달리 아래 명령어로 요소에 접근 할 수 없다. 이는 for or 문법이 내부적으로 ES6 이전 일반적인 순회방법과 다르게 구현되어 있음을 의미한다.
-> Set과 같은 경우는 증가하는 특정 i값으로 접근해서 순회하는 것이 아니라 iterable,iterator protocol을 따른다. 따라서, 키 값을 통해 접근하면 undefined가 나오지만 for of를 통해서 값이 출력되는 이유가 바로 이 것이다.
const set = new Set([1, 2, 3]);
console.log(set[0]); //undefined
console.log(set[2]); //undefined
map이 아닌 map.values(), map.keys(), map.entries()역시 정상동작을 하는데, 이는 해당 함수를 통해 반환되는 값들도 Iterator 형태임을 알 수 있다.
const map = new Map([['a',1], ['b', 2], ['c', 3]]);
for (const a of map)log(a)
//결과
['a',1]
['b',2]
['c',3]
💡 Map 역시 요소 접근이 불가능하다. 기존 리스트 순회와 같은 방법과는 다른 방법을 통해 for of 문법이 동작함을 알 수 있다.
const map = new Map([['a', 1], ['b', 2], ['c', 3]]);
console.log(map[0]); // undefined
console.log(map[2]); // undefined
이터레이터 프로토콜은 next 메소드를 소유하며 next 메소드를 호출하면 이터러블을 순회하며 value, done 프로퍼티를 갖는 이터레이터 리절트 객체를 반환하는 것이다. 이 규약을 준수한 객체가 이터레이터이다.
👉 즉, 이터레이터는 {value,done}객체를 리턴하는 next()라는 메서드를 가진 값이다.
이터러블 프로토콜을 준수한 이터러블은 Symbol.iterator 메소드를 소유한다. 이 메소드를 호출하면 이터레이터를 반환한다.
✏️ 이터레이터 프로토콜을 준수한 이터레이터는 next 메소드를 갖는다.
// 배열은 이터러블 프로토콜을 준수한 이터러블이다.
const array = [1, 2, 3];
// Symbol.iterator 메소드는 이터레이터를 반환한다.
const iterator = array[Symbol.iterator]();
// 이터레이터 프로토콜을 준수한 이터레이터는 next 메소드를 갖는다.
console.log('next' in iterator); // true
✏️ 이터레이터의 next 메소드를 호출하면 value, done 프로퍼티를 갖는 이터레이터 리절트(iterator result) 객체를 반환한다.
// 배열은 이터러블 프로토콜을 준수한 이터러블이다.
const array = [1, 2, 3];
// Symbol.iterator 메소드는 이터레이터를 반환한다.
const iterator = array[Symbol.iterator]();
// 이터레이터 프로토콜을 준수한 이터레이터는 next 메소드를 갖는다.
console.log('next' in iterator); // true
// 이터레이터의 next 메소드를 호출하면 value, done 프로퍼티를 갖는 이터레이터 리절트 객체를 반환한다.
let iteratorResult = iterator.next();
console.log(iteratorResult); // {value: 1, done: false}
✏️ 이터레이터의 next 메소드는 이터러블의 각 요소를 순회하기 위한 포인터의 역할한다. next 메소드를 호출하면 이터러블을 순차적으로 한 단계씩 순회하며 이터레이터 리절트 객체를 반환한다.
// 배열은 이터러블 프로토콜을 준수한 이터러블이다.
const array = [1, 2, 3];
// Symbol.iterator 메소드는 이터레이터를 반환한다.
const iterator = array[Symbol.iterator]();
// 이터레이터 프로토콜을 준수한 이터레이터는 next 메소드를 갖는다.
console.log('next' in iterator); // true
// 이터레이터의 next 메소드를 호출하면 value, done 프로퍼티를 갖는 이터레이터 리절트 객체를 반환한다.
// next 메소드를 호출할 때 마다 이터러블을 순회하며 이터레이터 리절트 객체를 반환한다.
console.log(iterator.next()); // {value: 1, done: false}
console.log(iterator.next()); // {value: 2, done: false}
console.log(iterator.next()); // {value: 3, done: false}
console.log(iterator.next()); // {value: undefined, done: true}
✏️ 이터레이터의 next 메소드가 반환하는 이터레이터 리절트 객체의 value 프로퍼티는 현재 순회 중인 이터러블의 값을 반환하고 done 프로퍼티는 이터러블의 순회 완료 여부를 반환한다.
전개연산자도 이터러블/이터레이터 프로토콜을 따른다.
a[Symbol.iterator] = null; 으로 iterator를 null로 해주면 에러가 발생한다.
즉, 전개연산자 역시 iterable 프로토콜을 따른다는 의미가 된다.
const a = [1, 2];
a[Symbol.iterator] = null;
log([...a, ...[3,4]);//Uncaught TypeError: a is not iterable
const b = [1, 2];
log([...a, ...[3,4]); // [1, 2, 3, 4]
앞서 말했듯이 일반 객체는 이터러블이 아니다. 일반 객체는 Symbol.iterator 메소드를 소유하지 않는다. 즉, 일반 객체는 이터러블 프로토콜을 준수하지 않으므로 for…of 문으로 순회할 수 없다.
// 일반 객체는 이터러블이 아니다.
const obj = { a: 1, b: 2 };
// 일반 객체는 Symbol.iterator 메소드를 소유하지 않는다.
// 따라서 일반 객체는 이터러블 프로토콜을 준수한 이터러블이 아니다.
console.log(Symbol.iterator in obj); // false
// 이터러블이 아닌 일반 객체는 for...of 문에서 순회할 수 없다.
// TypeError: obj is not iterable
for (const p of obj) {
console.log(p);
}
✏️ 하지만 일반 객체가 이터레이션 프로토콜을 준수하도록 구현하면 이터러블이 된다.
커스텀 이터러블 정의
/* 사용자 정의 이터러블 */
const iterable = {
[Symbol.iterator]() {
let i = 3;
return {
next() {
return i == 0 ? { done: true } : { value: i--, done: false };
},
[Symbol.iterator]() {
return this; // 자기 자신을 return 한다.
},
};
},
};
/* 이터러블 객체인지 for...of 문으로 확인하기 */
for (const a of iterable) console.log(a);
// 3
// 2
// 1
let iterator = iterable[Symbol.iterator]();
iterator.next(); // 3
for (const a of iterator) console.log(a);
// 2
// 1
👍 잘 만든 이터러블이란?
- 이터레이터를 만들어서 순회를 할 때 잘 동작 한다.
- 일부 진행한 이후에는 진행된 결과 이후의 값들로 진행이 된다.
⭐️ 이터레이터 역시 Symbol.iterator를 가지고 있고 이 Symbol.iterator를 실행한 값은 자기자신이다.
참조 및 참고하기 좋은 사이트