1-4. 타입과 값 - Iterable:Iterator:Generator 프로토콜, for...of, for...in
— Functional programming — 2 min read
프로그래머스에서 진행한 유인동님의 ES6로 알아보는 동시성 & 함수형 프로그래밍 강의를 들으며 정리한 내용입니다.
# 인트로
- Clojure는 새로운 타입을 만들지 않으며 함수형 프로그래밍을 한다.
- 우리도 자바스크립트의 내장객체만 사용하여 프로그래밍 할 것 이다.
- Class를 쓰지 않을 것이다 (= 사용자 정의 객체를 만들지 않는다)
# Iterable / Iterator
ES6에 새로 추가된 개념. Web API가 이들에 맞게 많은 프로토콜을 제공하기 때문에 제대로 이해하는 것이 중요하다.
for...of
& 전개연산자 (...
)[Symbol.iterator]()
메서드가 구현되어 있는 객체에만 사용가능- 내부적으로
[Symbol.iterator]()
가 구현되어 있는지를 확인하고,.next()
로 얻어지는 값들을 하나씩 수집하는 동작 방식
- String, Array, Map, Set은 기본적으로 Symbol.iterator를 가지고 있다
# Iterable / Iterator 프로토콜
- Iterable은
[Symbol.iterator]
라는 이름의 iterator를 가져야한다 - Iterator는 next method를 가져야한다
.next()
는 value와 done을 속성으로 갖는 객체를 반환해야한다 - Well-formed Iterable의
[Symbol.iterator]
메서드를 실행하면 자기자신(this)를 반환해야한다
1iterator[Symbol.iterator]() == iterator
1. 이터러블 직접 구현해보기
1const obj = {2 [Symbol.iterator]: function() { // --> 1번3 return {4 cur: 0, 5 next: function() { // --> 2번6 if(this.cur > 5) return { value: undefined, done: true }7 return {8 value: this.cur++,9 done: false10 }11 },12 [Symbol.iterator]: function() { 13 return this // --> 3번14 }15 }16 }17}18
19console.log(...obj) // [1,2,3,4,5]
# Generator
well-formed Iterable을 리턴하는 함수. 값 하나만을 리턴하는 일반함수와는 다르다
2. 이터러블을 제너레이터로 구현해보기
- Generator로 생성된 Iterable -> 지연평가
gen()
을 단순히 호출하는 것만으로 값이 평가되지 않는다
= 메모리에 쌓이지 않는다.next()
로 진행된 이터러블의 값만 평가된다
= 메모리 할당
- 제너레이터로 만든 이터러블은 well-formed이다
1function *gen(len) {2 let i = -13 while(++i < len) yield i4}5const b = gen(3)6
7console.log(b) // gen {<suspended>} --> 지연평가8console.log(...b) // 0 1 2 --> 메모리에 저장9console.log(b[Symbol.iterator]() === b) // true --> well-formed
Q : i의 초기값이 -1이고, ++i로 조건 설정하신 이유 ?
A : -1 쓰는게 습관이여서요. 위 코드 상에서는 i 를 while { 안쪽 }에서 사용하지는 않으니 차이가 없지만 i++ 을 하면 원하지 않는 i인 상태로 while 안쪽에서 사용될 가능성이 생기는 부분이 있어요. 그렇게 이용하고자 한다면 또 상관 없겠지만, 어쨌든 이 부분 때문에 습관적으로 상대적으로 안전하게 i 를 쓰기 위해 습관을 가지고 있습니다. 시작하는 i 부터 사용되는 i 까지는 같은게 보다 함수형 느낌
# reverseIter()
Array.prototype.reverse
의 문제
역행된 배열을 하나 더 만들어 메모리에 저장. 배열이 커지면 메모리에 큰 낭비가 생긴다- 제너레이터로 만든 Iterable은 실행하기전까지 값을 메모리에 저장하지 않는다. good !
1function *reverseIter(arr) {2 let l = arr.length3 while(l--) yield arr[l]4}5
6let a = reverseIter([1,2,3])7
8a // reverseIter {<suspended>} -> 멈춰있음. 지연성9a.next() // {value: 3, done: false} -> 실행된 값(3)만 메모리에 저장
# well-formed Iterable의 가치
iterable / iterator 프로토콜을 잘 지킨 이터러블은 for...of
와 잘 동작할 뿐만 아니라
- 다양한 로직의 코드를 짜는데 유용하고
- 데이터 순회에 다양한 해법들을 만들어 볼 수 있기 때문에 중요하다 (성능 최적화)
1. 다양한 로직 구현
go()
함수에서 재귀를 돌 때, 이터러블이 well-formed였기 때문에 진행된 지점을 기억하고 그 지점에서 다시 for루프를 돌 수 있었던 것이다 -> well-formed였기 때문에 무한루프에 빠지지 않았던 것 !
1function go(a, ...fs) {2 let b = a3 let iter = fs[Symbol.iterator]()4 return function recur(b) {5 for(const f of iter) { // -> 이터러블이 진행된 지점부터 다시 루프 시작6 b = f(b)7 if(b instanceof Promise) return b.then(recur) 8 }9 return b10 } (b)11}
2. 데이터 순회
users 데이터에서, "BB"이라는 이름을 가진 사람의 나이 뽑기
Object.value()
활용하는 방법- 객체가 가지는 속성의 값들로 이루어진 새로운 배열을 리턴
- 객체의 key의 개수만한 크기의 배열을 새로 만든다 -> 메모리 낭비
제너레이터 활용하는 방법
- 제너레이터를 사용하면, 똑같은 크기의 배열을 만들어내지 않고도 접근가능하다. good!
- 또한 break하는 순간, 제너레이터로 생성한 이터러블은 메모리에서 날아간다
1const users = {2 cid1: { name: 'AA', age: 32 },3 cid2: { name: 'BB', age: 30 },4 cid3: { name: 'CC', age: 20 },5 cid4: { name: 'DD', age: 21 },6 cid5: { name: 'EE', age: 18 },7}8
9function *valuesIterObj (obj) {10 for(const k in obj) yield obj[k]11}12
13// 1. Object.value()로 생성한 배열14for (const u of Object.values(users)) {15 if(u.name === 'BB') break;16}17
18// 2. 제너레이터로 생성한 이터러블 19for (const u of valuesIterObj(users)) {20 if(u.name === 'BB') break;21}
# well-formed vs non-well-formed
기본적으로 [Symbol.iterator]
이터레이터가 있는 이터러 블은 모두 for...of
로 순회 가능하지만
- well-formed 이터러블은
진행된 지점을 기억하고 있어 다양한 로직 구현이 가능하고
ex) 제너레이터로 만든 이터러블 - non-well-formed 이터러블은
진행된 지점을 기억하지 못해 다양한 로직 구현이 힘들다
ex) arguments, nodeList
1!function makeArgIter() {2 argIter = arguments3} (1, 2, 3)4
5const nodeIter = document.querySelectorAll('*')6
7function *gen(...arg){8 for(const v of arg) yield v9}10const genIter = gen(1, 2, 3)11
12argIter === argIter[Symbol.iterator]()13// false -> non-well-formed14nodeIter === nodeIter[Symbol.iterator]()15// false -> non-well-formed16genIter === genIter[Symbol.iterator]()17// true -> well-formed18
19
20// well-formed -> -> 진행된 지점 기억함21for(let i = 0; i < 2; i++) console.log(genIter.next().value) // 1 2 22for(let i = 0; i < 2; i++) console.log(genIter.next().value) // 3 undefined23
24// non-well-formed -> 진행된 지점 기억못함25for(let i = 0; i < 2; i++) 26 console.log("argIter", argIter[Symbol.iterator]().next().value) // 1 1
- well-formed인
genIter
는 이터러블이 진행된 지점을 기억하기 때문에, 또 다른 for루프에서 그 지점부터 다시 순회를 시작하지만 - non-well-formed인
argIter
는 진행된 지점을 기억하지 못해 for루프를 돌 때마다 첫번째 value인 1만 계속 출력된다
# for...in / for...of
for...in 문은 객체의 프로퍼티를 순회하기 위해 사용하고,
for...of 문은 배열의 요소를 순회하기 위해 사용한다
for...in
: 객체의 문자열 키(key)를 순회하기 위한 문법
: 배열엔 사용하지 않도록 한다 -> 배열의 요소뿐만 아니라 length같은 속성까지도 순회하기때문for...of
: 이터러블 객체를 순회한다 (Array, String, Map, Set, DOM node)
: done 프로퍼티가 true가 될 때까지 반복하며, done 프로퍼티가 true가 되면 반복을 중지
# for...in 문의 문제점
루프를 돌면 객체의 프로퍼티뿐만아니라, 프로토타입까지 출력하는 문제 (js 만들 때의 실수라고 함)
- ES5에서는
obj.hasOwnProperty(a)
로 해결- obj 프로퍼티에 a가 정의 되어있다면
true
- obj 프로퍼티에 a가 없거나, 프로토타입에만 정의 되있다면
false
- obj 프로퍼티에 a가 정의 되어있다면
- ES6에서는 Class로 만든 객체 사용
이런면에서 Class를 단순히 Syntax Sugar라고만 말하면 안된다. 치명적인 문제를 해결한 것이기 때문에
1function User(name, age) {2 this.name = name3 this.age = age4}5
6User.prototype.getName = function () {7 return this.name8}9
10const user1 = new User('bong', 56)11for(const k in user1) console.log(k)12// name, age, getName -> prototype까지 출력하는 문제13
14// 1. ES515for(const k in user1) {16 if(user1.hasOwnProperty(k)) console.log(k) }17// name, age -> 해결18
19// 2. ES620class User2 {21 constructor(name, age) {22 this.name = name;23 this.age = age;24 }25 getName() {26 return this.name;27 }28}29
30const user2 = new User2('ria', 90)31for(const k in user2) console.log(k)32// name, age -> 해결
이런점 때문에 사람들은 자바스크립트를 욕한다.
하지만 자바스크립트는 다양한 브라우저의 하위호환성을 가지며 발전해야 하는 언어이기 때문에, 이런 실수들을 안고 갈 수 밖에 없다