JS) iterable, iterator, generator

kangdari·2020년 4월 2일
1

iterable

내부 요소들을 공개적으로 탐색(반복)할 수 있는 데이터 구조

[Symbol.iterator] 메소드를 가지고 있음.

Array, Set, Map, String

[Symbol.iterator] 메소드가 존재하는 개체는 iterable

generator를 호출한 결과

function* generator() {
    yield 1
    yield 2
    yield 3
}
const gen = generator();

iterable한 개체의 특징

const arr = [1, 2, 3]
const map = new Map([ [1,1], ['2',2] ])
const set = new Set([1, 2 ,3])
const str = 'string도 iterable'
const gen = (function* () {
    yield 1
    yield 2
    yield 3
})();

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

Array.from(arr)
Array.from(map)
Array.from(set)
Array.from(str)
Array.from(gen)

2. spread operator로 배열 전환 가능

const arr1 = [...arr]
const arr2 = [...map]
const arr3 = [...set]
const arr4 = [...str]
const arr5 = [...gen]

3. 해체 할당 가능
원하는 위치의 요소 값을 가져올 수 있음.

const [a, ,c] = arr
const [ ,mapB] = map
...

4. for ...of문 수행 가능

for(let x of str){
    console.log(x) // s t r i n g ...
}
for(let x of set){
    console.log(x) // 1 2 3
}

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

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

const makeGenerator = iterable => function* () {
    yield* iterable 
    // yield*: iterable 안의 요소들을 모두 펼처서 각각을 yield로 만듦
    // yield 1;
    // yield 2;
    // yield 3;
}

const aGen =  makeGenerator(arr)();
console.log(aGen.next()) // { value: 1, done: false }
console.log(aGen.next()) // { value: 2, done: false }
console.log(aGen.next()) // { value: 3, done: false }

원리

[Symbol.iterator] 메소드를 가진 개체에 대해
내부적으로 Symbol.iterator or generator 를 실행하여 iterator로 변환한 상태에서
next()를 반복 호출하는 동일한 로직을 기반으로 함.

const iterator = iterable한 개체[Symbol.iterator]()
const iterator = set[Symbol.iterator]()

iterator.next() // {value: 1, done: false}
iterator.next() // {value: 2, done: false}
iterator.next() // {value: 3, done: false}

위 와같이 iterator.next() 메소드를 수행하면서
Array.from(), spread operator, for ...of 등을 수행합니다.

[Symbol.iterator] 메소드를 가지고 있다면 그 개체는 iterable하다
그래서 Symbol.iterator 없는 개체에 Symbol.iterator를 조건에 맞게 생성해주면
iterable할 수 있다.

iterable한 개체를 인자로 받을 수 있는 개체

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

WeakMap과 WeakSet은 iterable하지는 않지만 iterable한 개체를 인자로 받을 수 있음.

iterator

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

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

간단한 iterator 예시 1

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()) // { done: false, value: 10 }
console.log(iter.next()) // { done: false, value: 20 }
console.log(iter.next()) // { done: false, value: 30 }
console.log(iter.next()) // { done: true, value: undefined }

간단한 iterator 예시 2

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

기본 iterator에 접근

const str = '배고파'
const strIterator = str[Symbol.iterator]()

console.log(strIterator.next()) // { value: '배', done: false }
console.log(strIterator.next()) // { value: '고', done: false }
console.log(strIterator.next()) // { value: '파', done: false }
console.log(strIterator.next()) // { value: undefined, done: true }

객체가 iterable 한지 확인
객체가 [Symbol.iterator]를 가졌는지 확인해주면 됨.

const isIterable = target => !!target[Symbol.iterator]

console.log(isIterable({})) // false
console.log(isIterable([])) // true
console.log(isIterable('str')) // true

iterable한 개체 만들기

obj는 [Symbol.iterator] 메소드가 없기때문에 iterable 하지 않다.

[Symbol.iterator]는
done과 value 프로퍼티를 가진 next() 메소드를 포함한
iterator 객체를 반환하는 메소드이다.

obj에 [Symbol.iterator] 메소드를 추가해주면 iterable 하게 된다.

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

정리

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

덕 타이핑(Duck Typing)
객체가 어떤 타입에 걸맞는 변수와 메소드를 지니면 객체를 해당 타입에 속하는 것으로 간주한다.

Generator

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

const gen = gene()
gen.next() // {value: 1, done: false}
gen.next() // {value: 22, done: false}
gen.next() // value: undefined, done: true}
  • 선언 방식
function* gene() { yield } // 함수 선언식

const gen = function * (){ yield } // 함수 표현식

const obj = {
    gene1: function* () {},
    *gene2 () { yield }
}

class A {
    *gene () { yield }
}
  • 이터레이터로서의 제너레이터
function* gene() {
    console.log(1)
    yield 1
    console.log(2)
    yield 2
    console.log(3)
}

const gen = gene()

console.log(...gen)
// 1
// 2
// 3
// 1 2

객체 안의 Symbol.iterator를 제너레이터로 만들면 더 쉽게 이터레이터 생성이 가능하다.
이터레이터를 만들기 위한 조건들을 제너레이터로 표현하면 신경써줘야할 부분은 yield 뿐이다.

const obj = {
    a: 1,
    b: 2,
    c: 3,
    *[Symbol.iterator] () { 
        for(let prop in this){
            yield [prop, this[q]]
        }
    }
}
console.log(...obj) // [ 'a', 1 ] [ 'b', 2 ] [ 'c', 3 ]
// obj는 iterable하다.
// for...of 문도 사용 가능

yield* [iterable 개체] 개체 안의 내용을 풀어줌.

function* gene() {
    yield* [1,2],
    yield 
    yield* 'ab'
}
const gen = gene()
gen.next() // {value: 1, done: false}
gen.next() // {value: 2, done: false}
gen.next() // {value: undefined, done: false}
gen.next() // {value: a, done: false}
...
  • yeild*를 이용한 제너레이터 중첩
function* gene1 () {
    yield [1, 10]
    yield [2, 20]
}
function* gene2 () {
    yield [3, 30]
    yield [4, 40]
}
function* gene3 () {
    yield* gene1()
    // yield [1, 10]
    // yield [2, 20]
    yield* gene2()
    // yield [3, 30]
    // yield [4, 40]
    yield* [[5, 50], [6, 60]]
    // yield [5, 50]
    // yield [6, 60]
    yield [7, 70]
}
const gen = gene3()

gen.next().value // [1, 10]
...
  • 인자로 전달하기
function* gene() {
    let first = yield 1
    let second = yield first + 2
    yield second + 3
}
const gen = gene()
gen.next().value // 1
gen.next().value // NaN
gen.next().value // NaN

gen.next().value를 실행하면 let first = yield 1yield 1에서 1을 반환하고 정지한다. 그렇기 때문에 value의 값은 1이다.

두번째 gen.next().value가 실행되면 let first = 부분부터 실행된다. 하지만 이전 단계에서 값을 이미 반환했기 때문에 first에는 어떠한 값도 할당 되지 않는다. 그래서 first의 값은 undefined고
let second = yield first + 2에서 yield first + 2 까지 실행되지만 first가 undefined이기 때문에 first + 2는 NaN이다.

next() 메소드 호출 시 인자 값을 전달한 경우이다.

gen.next().value // 1
gen.next(10).value // 12
gen.next(20).value // 23

gen.next(10)가 실행되면 first에 10이 할당되고 yield first + 2에 의해 12가 반환된다.

비동기 처리 예시

const ajaxCalls = () => {
    const res1 = fetch.call('https://api.github.com/users?since=1000')
    const res2 = fetch.call(`https://api.github.com/users/${res1[3]}`)
}
const m = ajaxCalls()

위 코드는 동기적으로 처리 되는 코드이다.
server에 request를 보내고, server에서 response가 오기까지 시간이 걸린다.
때문에 res1에는 response 데이터가 아닌 불필요한 데이터가 담긴다.

원하는 데이터를 담기 위해서는 비동기처리가 필요하다.

  1. ajax
    서버에 request를 보내고 success가 오면 cb가 실행된다.
$.ajax({
    method: 'GET',
    url: 'https://api.github.com/users?since=1000',
    success: function(res){
        const res2 = fetch.call(`https://api.github.com/users/${res[3]}`)
    }
})
  1. Promise
fetch.call('https://api.github.com/users?since=1000')
    .then( res => {
        const res2 = fetch.call(`https://api.github.com/users/${res[3]}`)
    })
  1. Generator 비동기 처리
const fetchWrapper = (gen, url) => fetch(url)
    .then(res => res.json())
    .then(res => gen.next(res)); // 결과 값이 req1에 담김

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(); // 첫 단계 수행, yield에서 정지
    gen.next([gen, ...rest]); // [gen, from, nth] = [gen, ...rest]
}

runGenerator(getNthUserInfo, 1000, 4)
// 1004
// response 내용...

gen.next()가 실행되면 const [gen, from, nth] = yield;yield에서 정지

다음 gen.next([gen, ...rest])가 실행되면 [gen, from, nth] = [gen, ...rest]...
yield fetchWrapper(gen, ~)에서 정지

fetchWrapper에서 서버에서 요청을 보내고 res를 받아 그 값을 인자 값으로 next() 메소드를 호출 -> req1에는 서버의 res 값이 담기고 userId 값을 얻어내어 다음 yield에서 멈춤

다시 한번 fetchWrapper 함수가 실행되고 userId에 해당하는 res값이 req2에 담김. 끝.


async / await
편안...

const fetchJson = async (from, nth) => {
    const req1 = await fetch(`https://api.github.com/users?since=${from || 0}`)
        .then(res => res.json())
    const userId = req1[nth -1 || 0].id;
    const req2 = await fetch(`https://api.github.com/user/${userId}`)
    const data = await req2.json()
    console.log(data)
}

fetchJson(1000, 4)

0개의 댓글