JavaScript ES6 - 2. ES6에서의 순회와 이터러블:이터레이터 프로토콜


유인동님의 함수형 프로그래밍과 JavaScript ES6+ 인프런 강의를 듣고 개인적으로 정리한 내용입니다.
함수형 프로그래밍과 JavaScript ES6 이터레이터 프로토콜에 대해서 설명한다.

기존과 달라진 ES6에서의 리스트 순회

ES5 이하 버젼

1
2
3
4
5
6
7
8
9
const list = [1, 2, 3];
for (var i = 0; i < list.length; i++) {
console.log(list[i]);
}

const str = "abc";
for (var i = 0; i < str.length; i++) {
console.log(str[i]);
}
console
1
2
3
4
5
6
> 1
> 2
> 3
> a
> b
> c

ES5 이하의 버젼에서는 변수(list, str)에 담긴 값에 길이를 구하기위해서 length라는 프로퍼티를 이용해서 리스트 순회를 적용 하였다.

ES6

1
2
3
4
5
6
7
8
9
const list = [1, 2, 3];
for (const a of list) {
console.log(a);
}

const str = "abc";
for (const a of str) {
console.log(a);
}
console
1
2
3
4
5
6
> 1
> 2
> 3
> a
> b
> c

ES6에서는 ES5이하보다 더 선언적으로 for…of 문을 이용해서 순회한다. 코드를 간결하게 해주는 것뿐만 아니라 for…of 문에 대해서 어떻게 추상화를 했는지에 대해서는 더 알아보자.

Array, Set, Map을 통해 알아보는 이터러블/이터레이터 프로토콜

Array을 통해 알아보기

1
2
3
4
const arr = [1, 2, 3];
for (const a of arr) {
console.log(a);
}
console
1
2
3
> 1
> 2
> 3
1
2
3
4
5
const arr = [1, 2, 3];
arr[Symbol.iterator] = null;
for (const a of arr) {
console.log(a);
}
console
1
> Uncaught TypeError: arr is not iterable

Symbol는 ES6에서 추가된 어떤 객체에 대해서 Key로 사용될 수 있다. 여기서 arr[Symbol.iterator] = null;코드를 추가하면 결과에서 에러가 나타나는 것을 확인할 수 있다. 즉, Symbol.iterator가 for…of 문에 영향을 준다는 것을 알 수 있다. (Array, Set, Map 동일)

Set을 통해 알아보기

1
2
3
4
const set = new Set([1, 2, 3]);
for (const a of set) {
console.log(a);
}
console
1
2
3
4
5
6
7
8
9
> 1
> 2
> 3
< set[Symbol.iterator]
> set[Symbol.iterator]
length = 0
name = "values"
[[Scopes]] = Scopes[0]
__proto__ = function () { [native code] }

set[Symbol.iterator]안에 구현되어있는 함수 확인

Map을 통해 알아보기

1
2
3
4
const map = new Map([["a", 1], ["b", 2], ["c", 3]]);
for (const a of map) {
console.log(a);
}
console
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
> Array(2)
0 = "a"
1 = 1
length = 2
__proto__ = Array(0)
> Array(2)
0 = "b"
1 = 2
length = 2
__proto__ = Array(0)
> Array(2)
0 = "c"
1 = 3
length = 2
__proto__ = Array(0)
< map[Symbol.iterator]
> map[Symbol.iterator]
length = 0
name = "entries"
[[Scopes]] = Scopes[0]
__proto__ = function () { [native code] }

map[Symbol.iterator]안에 구현되어있는 함수 확인

이터러블/이터레이터 프로토콜

  • 이터러블: 이터레이터를 리턴하는 [Symbol.iterator]() 를 가진 값
  • 이터레이터: { value, done } 객체를 리턴하는 next() 를 가진 값
console
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
> let iterator = arr[Symbol.iterator]();
< iterator.next();
> iterator.next();
done = false
value = 1
__proto__ = Object {constructor: , __defineGetter__: , __defineSetter__: , hasOwnProperty: , __lookupGetter__: , ...}
< iterator.next();
> iterator.next();
done = false
value = 2
__proto__ = Object {constructor: , __defineGetter__: , __defineSetter__: , hasOwnProperty: , __lookupGetter__: , ...}
< iterator.next();
>iterator.next();
done = false
value = 3
__proto__ = Object {constructor: , __defineGetter__: , __defineSetter__: , hasOwnProperty: , __lookupGetter__: , ...}
< iterator.next();
> iterator.next();
done = true
value = undefined
__proto__ = Object {constructor: , __defineGetter__: , __defineSetter__: , hasOwnProperty: , __lookupGetter__: , ...}
  • 이터러블/이터레이터 프로토콜: 이터러블을 for…of, 전개 연산자 등과 함께 동작하도록한 규약

위에 console 에서 value에 들어오는 값을 담아서 출력을 해주는데 done = true가 되면 for…of 에서 빠져 나오게 되어있다.

Array 코드로 이터레이터 다시 알아보기

1
2
3
4
5
6
const arrIter = [1, 2, 3];
let iter1 = arrIter[Symbol.iterator]();
iter1.next();
for (const a of iter1) {
console.log(a);
}
console
1
2
> 2
> 3

Symbol.iterator를 실행한 arrIter는 iter1.next()를 호출하고 그다음에 실행할 iter1의 value값을 for…of의 변수 a에 담아 출력한다.
여기서 알 수 있는 게 for…of 안에서 Symbol.iteratornext()함수가 호출되는 것을 알 수 있다. (Set, Map도 동일)

Map에만 있는 이터레이터 확인

1
2
3
4
5
6
7
8
9
10
const mapIter = new Map([['a', 1], ['b', 2], ['c', 3]]);
for (const a of mapIter.keys()) {
console.log(a);
}
for (const a of mapIter.values()){
console.log(a);
}
for (const a of mapIter.entries()) {
console.log(a);
}
console
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
> a
> b
> c
> 1
> 2
> 3
> Array(2)
0 = "a"
1 = 1
length = 2
__proto__ = Array(0)
> Array(2)
0 = "b"
1 = 2
length = 2
__proto__ = Array(0)
> Array(2)
0 = "c"
1 = 3
length = 2
__proto__ = Array(0)

사용자 정의 이터러블을 통해 알아보기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const iterable = {
[Symbol.iterator]() {
let i = 3;
return {
next() {
return i == 0 ? {done: true} : {value: --i, done: false};
}
}
}
};
let iterator = iterable[Symbol.iterator]();
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
console
1
2
3
4
5
6
7
8
9
10
11
12
13
> {value: 2, done: false}
done: false
value: 2
__proto__: Object
> {value: 1, done: false}
done: false
value: 1
__proto__: Object
> {value: 0, done: false}
done: false
value: 0
__proto__: Object
> {done: true}

여기서 iterator변수에 [Symbol.iterator]가 들어가있기때문에 iterator변수는 for…of 문에 들어갈수 있는 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const iterable = {
[Symbol.iterator]() {
let i = 3;
return {
next() {
return i == 0 ? {done: true} : {value: --i, done: false};
}
}
}
};

for(const a of iterable) {
console.log(a);
}
console
1
2
3
> 2
> 1
> 0

그리고 iterable이 이터레이터가되게 만들기위해서는 [Symbol.iterator]()를 반환을 해야된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const iterable = {
[Symbol.iterator]() {
let i = 3;
return {
next() {
return i == 0 ? {done: true} : {value: i--, done: false};
},
[Symbol.iterator]() {
return this;
}
}
}
};
let iterator = iterable[Symbol.iterator]();
iterator.next();
iterator.next();
for (const a of iterator) {
console.log(a);
}
console
1
> 0

전개 연산자

전개 연산자는 배열을 더욱더 직관적으로 사용 할 수 있고, 배열을 분해 하지 않고 그냥 할당 해 버리는 방식이다.
즉, 배열 중간에 배열 값을 할당하기 위해서 많이 사용된다.

1
2
const a = [1, 2];
console.log([...a, ...arr, ...set, ...map.keys()]);
console
1
2
3
4
5
6
7
8
9
10
11
12
13
14
> (11) [1, 2, 1, 2, 3, 1, 2, 3, "a", "b", "c"]
0: 1
1: 2
2: 1
3: 2
4: 3
5: 1
6: 2
7: 3
8: "a"
9: "b"
10: "c"
length: 11
__proto__: Array(0)

ES6에서의 순회와 이터러블:이터레이터 프로토콜 소스코드

참조