ES5에서의 리스트 순회

// ES5에서의 리스트 순회

const list = [1,2,3,4,5];

for(var i=0; i<list.length;i++){
    console.log(i)
}

const str = 'abc';

for(var i=0; i<str.length;i++){
    console.log(str[i])
}

ES5에서의 for문은 시작과 끝을 정의해주고 하나씩 증가하여 리스트를 순회 했었다. 이것은 length 라는 속성에 의존해야 하는 문제점이 있었다. 즉, 순회하려면 __proto__ 또는 constructor 에서 length 를 프로토타입 기반으로 상속을 받아야 된다는 점이다.


ES6에서의 리스트 순회 (for of iterable)

  • Array에서의 순회
const arr = [1,2,3];

for(const a of arr) console.log(a);

// 1
// 2
// 3

const str = 'abc'

for(const a of str) console.log(a);

// a
// b
// b
  • Set에서의 순회
const set = new Set([1,2,3]);

for(const a of set) console.log(a);

// 1
// 2
// 3
  • Map에서의 순회
const map = new Map([['a',1], ['b',2],['c',3]]);

for(const a of map) console.log(a);

// ['a', 1]
// ['b', 2]
// ['c', 3]

ES2015에 도입된 for of Iterable이 어떤 식으로 작동하는지 자세히 알아볼 필요가 있다.


iterable 객체

반복 가능한(iterable, 이터러블) 객체는 배열을 일반화한 객체이다. for..of 구문과 함께 ES2015에서 도입되었다.

배열은 대표적인 이터러블이다. 배열 외에도 다수의 내장 객체가 반복 가능한 이터러블이다. 문자열 역시 이터러블의 예이다.

배열이 아닌 객체(목록, 집합 등)도 for..of 문법을 적용할 수 있다면 컬렉션을 순회하는데 유용하다.

반복 가능한 객체와 아닌 객체를 구분짓는 큰 특징은, 객체의 Symbol.iterator 속성에 특별한 형태의 함수가 들어있다는 점이다.

객체의 Symbol.iterator 속성에 특정 형태의 함수가 들어있다면, 이를 반복 가능한 객체 즉 iterable이라 부른다. 이를 iterable protocol를 따른다고 말한다.


Symbol.iterator

밑의 코드를 보면 Array 타입의 arr는 인덱스로 접근이 가능하지만 Set, Map은 인덱스로 접근이 불가능하다. 그런데 for..of로 순회가 가능한 이유는 iterable protocol를 따르기 때문이다.

const arr = [1,2,3];
const set = new Set([1,2,3]);
const map = new Map([['a',1], ['b',2], ['c',3]])

console.log(arr[0]) // 1
console.log(set[0]) // undefined
console.log(map[0]) // undefined

for(const a of arr) console.log(a); // 1 2 3
for(const a of set) console.log(a); // 1 2 3 
for(const a of map) console.log(a); // ['a', 1] ['b', 2] ['c', 3]


아래의 코드를 보면 Array객체에 Symbol.iterator를 null로 주었을 때 arriterable이 아니다라고 에러가 발생하는 것을 확인할 수 있다.

다시말해, iterable protocol를 따른다는 것은 이터러블을 for..of, 전개 연산자 등과 함께 동작한다는 규약이라고 이해하면 된다.

const arr = [1,2,3];
    
arr[Symbol.iterator] = null;

for(const a of arr) console.log(a);

// Uncaught TypeError: arr is not iterable
const arr = [1,2,3];
arr[Symbol.iterator] = null;
const arr2 = [...arr];
// Uncaught TypeError: arr is not iterable


Iterator Protocol

iterable 객체는 iterable Protocol를 따른다고 했다. (이터러블과 이터레이터를 혼동할 수 있는데 잘 구분해서 이해해야한다.)

iterable Protocol를 따른다는 말은 Symbol.iterator 속성에 특별한 형태의 함수가 저장되어 있다는 말이다.

iterable Protocol를 만족하려면 Symbol.iterator 속성에 저장되어 있는 함수는 iterator 객체를 반환해야 한다.

iterator는 아래의 조건을 만족하는 객체이다.

  • next 메서드를 갖는다.
  • next 메서드는 value, done 두 속성을 갖는 객체를 반환한다.

위 조건을 iterator protocol이라고 한다.

아래의 예제를 통해 쉽게 이해해보자.

// iterable Array 객체로부터 iterator를 생성한다.
const arr = [1,2,3];
const iterator = arr[Symbol.iterator]();

// iterator의 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}
// iterable Set 객체로부터 iterator를 생성한다.
const set = new Set([1,2,3]);
const iterator = set[Symbol.iterator]();

// iterator의 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}
// iterable Map 객체로부터 iterator를 생성한다.
const map = new Map([['a',1],['b',2],['c',3]]);
const iterator = map[Symbol.iterator]();

// iterator의 next 메서드를 통해 객체를 반환한다.
console.log(iterator.next()); // {value: Array(2), done: false}
console.log(iterator.next()); // {value: Array(2), done: false}
console.log(iterator.next()); // {value: Array(2), done: false}
console.log(iterator.next()); // {value: undefined, done: false}


Iterable protocoliterator protocol에 대해 이해가 됐다면, iterable를 직접 구현해보자.

const iterable = {
    [Symbol.iterator](){
        let range = 1;
        return {
            next(){
                return range<4 ? {value: range++, done: false} : {value: undefined, done: true}
            }
        }
    }
}

const iterator = iterable[Symbol.iterator]();

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}

그럼 직접만든 이터러블 객체를 for..of로 순회해보자. iterable이 아니라고 에러가 발생할 것이다.

const iterable = {
    [Symbol.iterator](){
        let range = 1;
        return {
            next(){
                return range<4 ? {value: range++, done: false} : {value: undefined, done: true}
            }
        }
    }
}

const iterator = iterable[Symbol.iterator]();

for(const a of iterator) log(a); // Uncaught TypeError: iterator is not iterable

그 이유는 iterator도 Symbol.iterator를 가지고 있으며, 이것은 자기 자신이다.
즉, iterator는 자기 자신을(this)를 반환하는 Symbol.iterator를 가지고 있다는 말이다.

const arr = [1,2,3];
const arr2 = arr[Symbol.iterator]();

console.log(arr2[Symbol.iterator]()==arr2) // true

이제 자기 자신을(this)를 반환하는 Symbol.iterator 메서드를 추가해주자.

const iterable = {
    [Symbol.iterator](){
        let range = 1;
        return {
            next(){
                return range<4 ? {value: range++, done: false} : {value: undefined, done: true}
            },
            [Symbol.iterator](){
                return this
            }
        }
    }
}

const iterator = iterable[Symbol.iterator]();

for(const a of iterator) console.log(a); // 1 2 3 


재밌는 사실

javascript가 사용되는 환경인 Web API 라던지, DOM과 같은 많은 값들도 이터러블&이터레이터 프로토콜을 따른다.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    
</body>
</html>

querySelectorAll() 메서드는 NodeList를 반환한다. 즉, 배열이 아닌데도 for..of 순회가 가능하다.

그 이유는 이터러블&이터레이터 프로토콜따르고 Symbol.iterator가 구현되어있기 때문이다.

const dom = document.querySelectorAll('*');
const iterator = dom[Symbol.iterator]();

for(const a of iterator) console.log(a);

image