Skip to content

Latest commit

 

History

History
executable file
·
652 lines (551 loc) · 14.7 KB

14. iterable iterator generator.md

File metadata and controls

executable file
·
652 lines (551 loc) · 14.7 KB

14. iterable, iterator, generator

14-1. iterable

14-1-1. 소개

내부 요소들을 공개적으로 탐색(반복)할 수 있는 데이터 구조.
[Symbol.iterator] 메소드를 가지고 있다.

const arr = ['a', 'b', 'c']
const set = new Set(['a', 'b', 'c'])
const map = new Map([[false, 'no'], [true, 'yes'], ['well', 'soso']])
const str = '문자열도 이터러블하다!?!!'

14-1-2. 개체 자신이 iterable한 경우

1) array, map, set, string

2) [Symbol.iterator] 메소드가 존재하는 모든 개체

console.dir([1, 2, 3])
console.dir(new Set([1, 2, 3]))
console.dir(new Map([[0, 1], [1, 2], [2, 3]]))
  • 없는 경우
const obj = { 0: 1, 1: 2, 2: 3, length: 3 }
console.dir(obj)

3) generator를 호출한 결과

function* generator () {
  yield 1
  yield 2
  yield 3
}
const gene = generator()
console.dir(gene)

14-1-3. iterable한 개체의 특징

const arr = [1, 2, 3]
const map = new Map([['a', 1], ['b', 2], ['c', 3]])
const set = new Set([1, 2, 3])
const str = '이런것도 된다.'
const gene = (function* () {
  yield 1
  yield 2
  yield 3
})()

1) Array.from 메소드로 배열로 전환 가능

const arrFromArr1 = Array.from(arr)
const arrFromMap1 = Array.from(map)
const arrFromSet1 = Array.from(set)
const arrFromStr1 = Array.from(str)
const arrFromGene1 = Array.from(gene)

2) spread operator로 배열로 전환 가능

const arrFromArr2 = [...arr]
const arrFromMap2 = [...map]
const arrFromSet2 = [...set]
const arrFromStr2 = [...str]
const arrFromGene2 = [...gene]

3) 해체할당 가능

const [arrA, , arrC] = arr
const [mapA, , mapC] = map
const [ , setB, setC] = set
const [ , strB, ...strRest] = str
const [geneA, ...geneRest] = gene
console.log(arrA, arrC)
console.log(mapA, mapC)
console.log(setB, setC)
console.log(strB, strRest)
console.log(geneA, geneRest)

4) for ... of 명령 수행 가능

for (const x of arr) {
  console.log(x)
}
for (const x of map) {
  console.log(x)
}
for (const x of set) {
  console.log(x)
}
for (const x of str) {
  console.log(x)
}
for (const x of gene) {
  console.log(x)
}

5) Promise.all, Promise.race 명령 수행 가능

const a = [
  new Promise((resolve, reject) => { setTimeout(resolve, 500, 1) }),
  new Promise((resolve, reject) => { setTimeout(resolve, 100, 2) }),
  3456,
  'abc',
  new Promise((resolve, reject) => { setTimeout(resolve, 300, 3) }),
]
Promise.all(a)
  .then(v => { console.log(v) })
  .catch(err => { console.error(err) })

const s = new Set([
  new Promise((resolve, reject) => { setTimeout(resolve, 300, 1) }),
  new Promise((resolve, reject) => { setTimeout(resolve, 100, 2) }),
  new Promise((resolve, reject) => { setTimeout(reject, 200, 3) }),
])
Promise.race(s)
  .then(v => { console.log(v) })
  .catch(err => { console.error(err) })

6) generator - yield* 문법으로 이용 가능

const arr = [1, 2, 3]
const map = new Map([['a', 1], ['b', 2], ['c', 3]])
const set = new Set([1, 2, 3])
const str = '이런것도 된다.'

const makeGenerator = iterable => function* () {
  yield* iterable
}
const arrGen = makeGenerator(arr)()
const mapGen = makeGenerator(map)()
const setGen = makeGenerator(set)()
const strGen = makeGenerator(str)()

console.log(arrGen.next())
console.log(mapGen.next())
console.log(...setGen)
console.log(...strGen)

여기까지 모두 내부적으로는 Symbol.iterator 또는 generator을 실행하여 iterator로 변환한 상태에서 next()를 반복 호출하는 동일한 로직을 기반으로 함.

7) iterable 객체에 [Symbol.iterator] 정의되지 않은 경우

const obj = {
  a: 1,
  b: 2,
  [Symbol.iterator] () {
    return 1
  }
}
console.log([...obj])

14-1-4. iterable한 개체를 인자로 받을 수 있는 개체

new Map()
new Set()
new WeakMap()
new WeakSet()
Promise.all()
Promise.race()
Array.from()

14-2. Iterator

14-2-1. 소개

반복을 위해 설계된 특별한 인터페이스를 가진 객체.

  • 객체 내부에는 next() 메소드가 있는데,
  • 이 메소드는 valuedone 프로퍼티를 지닌 객체를 반환한다.
  • done 프로퍼티는 boolean값이다.

초간단 이터레이터 예시

const iter = {
  items: [10, 20, 30],
  count: 0,
  next () {
	const done = this.count >= this.items.length
    return {
      done,
      value: !done ? this.items[this.count++] : undefined
    }
  }
}
console.log(iter.next())
console.log(iter.next())
console.log(iter.next())
console.log(iter.next())
console.log(iter.next())

14-2-2. iterator 구현해보기

1) 객체에는 'next' 메소드가 존재해야 한다.

const iter = {
  next () {}
}

2) next 메소드는 다시 객체를 반환해야 한다.

const iter = {
  next () {
    return {}
  }
}

3) 반환되는 객체에는 boolean 값을 가지는 done 프로퍼티가 존재해야 한다.

const iter = {
  next () {
	return {
      done: false
    }
  }
}
console.log(iter.next())

4) value 프로퍼티를 추가하고, 일정시점에 done을 true로 변환할 수 있게끔 한다.

const iter = {
  val: 0,
  next () {
    const isDone = ++this.val >= 5
    return {
      done: isDone,
      value: !isDone ? this.val : undefined
    }
  }
}
console.log(iter.next())

14-2-3. 기본 이터레이터

  • 기본 이터레이터에 접근하기
const arr = [ 1, 2 ]
const arrIterator = arr[Symbol.iterator]()
console.log(arrIterator.next())
console.log(arrIterator.next())
console.log(arrIterator.next())
  • 객체가 이터러블한지 확인하기
const isIterable = target => typeof target[Symbol.iterator] === 'function'

14-2-4. 이터러블한 개체 만들기

1) 개체의 Symbol.iterator 메소드를 호출하면 iterator가 반환되도록 한다.

for...of, ...(spread) 등은 모두 개체 내부 (또는 개체의 __proto__)의 [Symbol.iterator]를 실행한 결과를 바탕으로, donetrue가 될 때까지 계속하여 next()를 호출하는 식으로 구현되어 있다.

// 절대 실행하지 말 것!!
const createIterator = () => {
  return {
    next () {
      return {
        done: false
      }
    }
  }
}
const obj = {
  [Symbol.iterator]: createIterator
}
console.log(...obj)
// 절대 실행하지 말 것!!
const obj = {
  [Symbol.iterator]() {
    return this
  },
  next() {
    return {
      done: false
    }
  }
}
console.log(...obj)

2) done이 true가 나오지 않는 한 이터레이트시 무한정 반복실행한다. 따라서 적절한 시점에 done을 true로 바꾸어주어야 한다.

const createIterator = () => {
  let count = 0
  return {
    next () {
      return {
        done: count > 3
      }
    }
  }
}
const obj = {
  [Symbol.iterator]: createIterator
}
console.log(...obj)

3) value 프로퍼티를 추가하면 완성!

const createIterator = function () {
  let count = 0
  const items = Object.entries(this)
  return {
    next () {
      return {
        done: count >= items.length,
        value: items[count++]
      }
    }
  }
}
const obj = {
  a: 1,
  b: 2,
  c: 3,
  d: 4,
  [Symbol.iterator]: createIterator
}
console.log(...obj)

4) 객체 내부에 직접 할당한 형태

const obj = {
  a: 1,
  b: 2,
  c: 3,
  d: 4,
  [Symbol.iterator] () {
    let count = 0
    const items = Object.entries(this)
    return {
      next () {
        return {
          done: count >= items.length,
          value: items[count++]
        }
      }
    }
  }
}
console.log(...obj)

5) 또는 generator를 실행한 결과 역시 이터러블하다.

const obj = {
  a: 1,
  b: 2,
  c: 3,
  d: 4,
  *[Symbol.iterator] () {
    yield* Object.entries(this)
  }
}
console.log(...obj)

6) 정리

  • for-of, ...(spread operator), forEach 메소드 등은 내부적으로
  • [Symbol.iterator]를 실행한 결과 객체를 들고,
  • 객체 내부의 next() 메소드를
  • done 프로퍼티true가 나올 때까지 반복하여 호출한다.
  • 즉, Symbol.iterator 메소드의 내용을 위 요구사항에 맞추어 구현하기만 하면 iterable한 객체이다.

Duck type Protocol : 덕타이핑

14-3. Generator

14-3-1. 소개

  • 중간에서 멈췄다가 이어서 실행할 수 있는 함수.
  • function 키워드 뒤에 *를 붙여 표현하며, 함수 내부에는 yield 키워드를 활용한다.
  • 함수 실행 결과에 대해 next() 메소드를 호출할 때마다 순차적으로 제너레이터 함수 내부의 yield 키워드를 만나기 전까지 실행하고, yield 키워드에서 일시정지한다.
  • 다시 next() 메소드를 호출하면 그 다음 yield 키워드를 만날 때까지 함수 내부의 내용을 진행하는 식이다.
function* gene () {
  console.log(1)
  yield 1
  console.log(2)
  yield 2
  console.log(3)
}
const gen = gene()
  • 선언 방식
function* gene () { yield }
const gene = function* () { yield }
const obj = {
  gene1: function* () { yield }
  *gene2 () { yield }
}
class A {
  *gene () { yield }
}

14-3-2. 이터레이터로서의 제너레이터

function* gene () {
  console.log(1)
  yield 1
  console.log(2)
  yield 2
  console.log(3)
}
const gen = gene()
console.log(...gen)
const obj = {
  a: 1,
  b: 2,
  c: 3,
  *[Symbol.iterator] () {
    for (let prop in this) {
      yield [prop, this[prop]]
    }
  }
}
console.log(...obj)
for (let p of obj) {
  console.log(p)
}

14-3-3. yield* [iterable]

  • yield* 은 다시 뒤에 이터러블 객체를 받아서, 하나씩 (next() 호출시) 꺼냄
function* gene () {
  yield* [1, 2, 3, 4, 5]
  yield
  yield* 'abcde'
}
for (const c of gene()) {
  console.log(c)
}

// 1,2,3,4,5,undefined, a,b,c,d,e

yield generator 의 경우

  • generator의 done 값이 false가 나올때 까지 yield한 효과를 냄
function* gene1 () {
  yield [1, 10]
  yield [2, 20]
}
function* gene2 () {
  yield [3, 30]
  yield [4, 40]
}
function* gene3 () {
  console.log('yield gene1')
  yield* gene1() // 이 yield* 하나가 아래 실행과 동일 
  // yield [1, 10]
  // yield [2, 20]
  console.log('yield gene2')
  yield* gene2()
  // yield [3, 30]
  // yield [4, 40]
  console.log('yield* [[5, 50], [6, 60]]')
  yield* [[5, 50], [6, 60]]
  // yield [5, 50]
  // yield [6, 60]
  console.log('yield [7, 70]')
  yield [7, 70]
}
const gen = gene3()
for (let [k, v] of gen) {
  console.log(k, v)
}

14-3-4. 인자 전달하기

function* gene () {
  let first = yield 1 // yield가 밖에 나갔다오면 yield1은 사라지고 undefined가 first에 할당됨
  let second = yield first + 2 // 때문에 first + 2는 NaN 이 됨
  yield second + 3
}
const gen = gene()
console.log(gen.next().value) //1 
console.log(gen.next().value) //3 
console.log(gen.next().value) //6 

function* gene () {
  let first = yield 1 // 나갔다 들어올 때 first yield1 는 외부에서 넘겨준 10으로 치환됨
  let second = yield first + 2 //12를 밖으로 내보내고 들어올때 first + 2는 외부에서 받은 20으로 치환됨 
  yield second + 3 //외부에 23을 내보냄 
}
const gen = gene()
console.log(gen.next(10).value) //1 
console.log(gen.next(20).value) //3 
console.log(gen.next().value) //6 

14-3-5. 비동기 작업 수행

  • 이전의 비동기 작업 수행
// 깃헙에서 유저정보를 가져오는 api

// 하려고 하는 것은 다음과 같음
// 우선 userId가 1000번 이후인 데이터를 가져와서
// 그 중에 4번째에 위치한 User 정보를 보고싶다.
// 1003이라고 썻지만 사실 1001, 1002, 1003이 모두 데이터를 가지고 있지 않을 수 있다. 1002번이 탈퇴해서 빌수도
// 진짜 하고싶은 것은 /user/${rest1[3]} 처럼 4번째 유저를 가져오는 것임
const ajaxCalls = () => {
  const res1 = fetch.get('https://api.github.com/users?since=1000')
  const res2 = fetch.get('https://api.github.com/user/${rest1[3]}')
}
const m = ajaxCalls()

// fetch 메소드는 html5 메소드로 get으로 url을 넣어 call 하면
// sever에 request 보내고,
// sever에서 response가 옴 
// 그러나 ,request 와 response 사이의 시간은 서버의 상태와 네트워크 속도에 따라 천차만별
// 그래서 일단 fetch.get() 요청을 하자마자, rest1에 결과값을 담아버림, 
// 즉 response된 결과가 담기는게 아니라, 불필요한 데이터가 담김
// 그래서 rest2 의 줄에서  /user/${rest1[3]} 를 조회해도 무의미함 
// 이것이 비동기 처리의 고민이다.
// 그래서 옛날에 어떻게 했냐하면? 

//콜백방식의 Jquery 비동기 처림
$.ajax({
  method: 'GET',
  url: 'https://api.github.com',
  success: function(res1){
    const res2 = fetch.get('https://api.github.com/user/${res1[3]}')
  }
})
// 이런식으로 success가 완료되면 콜백 function을 실행하는 식으로 처리함

// 그러다가 다음으로 Promise 객체 등장 
// then 키워드가 sucess를 의미하게 됨, fetch.get()의 결과가 Promise 객체를 반환함
fetch.get('https://api.github.com/users?since=1000').then(function(res1){
  const res2 = fetch.get('https://api.github.com/user/${rest1[3]}')
})
  • Generator 방식의 비동기 처리
const fetchWrapper = (gen, url) => fetch(url) // 서버에 get 요청을 보냄 
  .then(res => res.json())
  .then(res => gen.next(res));

// 비동기 처리인데 동기적으로 처리하는 것 처럼 조작할 수 있음
function* getNthUserInfo() {
  const [gen, from, nth] = yield;
  const req1 = yield fetchWrapper(gen, `https://api.github.com/users?since=${from || 0}`);
  const userId = req1[nth - 1 || 0].id;
  console.log(userId);
  const req2 = yield fetchWrapper(gen, `https://api.github.com/user/${userId}`);
  console.log(req2);
}
const runGenerator = (generator, ...rest) => {
  const gen = generator();
  gen.next();
  gen.next([gen, ...rest]);
}
runGenerator(getNthUserInfo, 1000, 4);
runGenerator(getNthUserInfo, 1000, 6);
const fetchWrapper = url => fetch(url).then(res => res.json());
function* getNthUserInfo() {
  const [from, nth] = yield;
  const req1 = yield fetchWrapper(`https://api.github.com/users?since=${from || 0}`);
  const userId = req1[nth - 1 || 0].id;
  console.log(userId);
  const req2 = yield fetchWrapper(`https://api.github.com/user/${userId}`);
  console.log(req2);
}
const runGenerator = (generator, ...rest) => {
  const gen = generator();
  gen.next();
  gen.next([...rest]).value
    .then(res => gen.next(res).value)
    .then(res => gen.next(res));
}
runGenerator(getNthUserInfo, 1000, 4);
runGenerator(getNthUserInfo, 1000, 6);