Skip to content
Underbleu
GithubLinkedin

1-4. 타입과 값 - Iterable:Iterator:Generator 프로토콜, for...of, for...in

Functional programming2 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 프로토콜

  1. Iterable은 [Symbol.iterator]라는 이름의 iterator를 가져야한다
  2. Iterator는 next method를 가져야한다
    .next()는 value와 done을 속성으로 갖는 객체를 반환해야한다
  3. 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: false
10 }
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 = -1
3 while(++i < len) yield i
4}
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.length
3 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. 다양한 로직의 코드를 짜는데 유용하고
  2. 데이터 순회에 다양한 해법들을 만들어 볼 수 있기 때문에 중요하다 (성능 최적화)

1. 다양한 로직 구현

go() 함수에서 재귀를 돌 때, 이터러블이 well-formed였기 때문에 진행된 지점을 기억하고 그 지점에서 다시 for루프를 돌 수 있었던 것이다 -> well-formed였기 때문에 무한루프에 빠지지 않았던 것 !

1function go(a, ...fs) {
2 let b = a
3 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 b
10 } (b)
11}

2. 데이터 순회

users 데이터에서, "BB"이라는 이름을 가진 사람의 나이 뽑기

  1. Object.value() 활용하는 방법

    • 객체가 가지는 속성의 값들로 이루어진 새로운 배열을 리턴
    • 객체의 key의 개수만한 크기의 배열을 새로 만든다 -> 메모리 낭비
  2. 제너레이터 활용하는 방법

    • 제너레이터를 사용하면, 똑같은 크기의 배열을 만들어내지 않고도 접근가능하다. 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 = arguments
3} (1, 2, 3)
4
5const nodeIter = document.querySelectorAll('*')
6
7function *gen(...arg){
8 for(const v of arg) yield v
9}
10const genIter = gen(1, 2, 3)
11
12argIter === argIter[Symbol.iterator]()
13// false -> non-well-formed
14nodeIter === nodeIter[Symbol.iterator]()
15// false -> non-well-formed
16genIter === genIter[Symbol.iterator]()
17// true -> well-formed
18
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 undefined
23
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 만들 때의 실수라고 함)

  1. ES5에서는 obj.hasOwnProperty(a)로 해결
    • obj 프로퍼티에 a가 정의 되어있다면 true
    • obj 프로퍼티에 a가 없거나, 프로토타입에만 정의 되있다면 false
  2. ES6에서는 Class로 만든 객체 사용
    이런면에서 Class를 단순히 Syntax Sugar라고만 말하면 안된다. 치명적인 문제를 해결한 것이기 때문에
1function User(name, age) {
2 this.name = name
3 this.age = age
4}
5
6User.prototype.getName = function () {
7 return this.name
8}
9
10const user1 = new User('bong', 56)
11for(const k in user1) console.log(k)
12// name, age, getName -> prototype까지 출력하는 문제
13
14// 1. ES5
15for(const k in user1) {
16 if(user1.hasOwnProperty(k)) console.log(k) }
17// name, age -> 해결
18
19// 2. ES6
20class 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 -> 해결

이런점 때문에 사람들은 자바스크립트를 욕한다.
하지만 자바스크립트는 다양한 브라우저의 하위호환성을 가지며 발전해야 하는 언어이기 때문에, 이런 실수들을 안고 갈 수 밖에 없다