[JS] stack과 queue를 iterator로 작성하시오.

준리·2022년 8월 24일
0
post-thumbnail

class로 작성한 stack과 queue를 iterator로 작성하시오.

Class 로 만든 스택과 큐 다시보기

console.log([...stack], [...queue]); 
for (const s of stack) console.log(s);
for (const q of queue) console.log(q);
const itStack = stack[Symbol.iterator]();
console.log(itStack.next());
console.log(itStack.next());
...
const itQueue = queue.iterator();
console.log(itQueue.next());

class는 무엇을 return하는가?

🚀 stack Stack {}
🚀 queue Queue {}
E:\AI\220702 fullstack\jsbasic\0813\velogClass2.js:75   
console.log([...stack], [...queue]);
                ^
// terminal : TypeError: stack is not iterable

지난 번에 작성한 class 에서 스프레드 문법을 돌려봤다. 해당 작업은 typeError를 돌려줬고 stack은 이터러블 하지 않다고 한다. 그럼 여기서 iterable이 뭔지 알아봐야겠다.

The iterable protocol : 반복가능한

Iterator

iterable protocol 은 JavaScript 객체들이, 예를 들어 for..of 구조에서 어떠한 value 들이 loop 되는 것과 같은 iteration 동작을 정의하거나 사용자 정의하는 것을 허용합니다

반복하지 못하는 객체에 대해 반복가능한 프로토콜을 만들어주는 것이다.

어떤 built-in type 들은 Array 또는 Map 과 같은 default iteration 동작으로 built-in iterables 입니다.
String, Array, TypedArray, Map and Set 는 모두 내장 iterable이다. 이 객체들의 프로토타입 객체들은 모두 @@``iterator 메소드를 가지고 있기 때문이다.

Array나 Map 등 은 기본적으로 빌트인 이터러블을 지원하기 때문에 for...of문이나 스프레드 문법을 지원하지만, 다른 것들은 iterable 하게 만들어줘야한다.
딱딱한 객체를 조금 더 야들야들하게 만든다고 해야할까?

iterable 하기 위해서 object는 @@iterator 메소드를 구현해야 합니다. 이것은 object (또는 prototype chain 의 오브젝트 중 하나) 가 Symbol.iterator key 의 속성을 가져야 한다는 것을 의미합니다

Property : [Symbol.iterator]
Value : object를 반환하는, arguments 없는 function. iterator protocol 을 따른다.

굳이 Symbol type을 추가해준 이유는 사용자가 iterator 함수를 직접 구현하지말라고 강제하는 것이라고 한다. 중복방지

iterable 하다면 뭐가 좋을까

이터레이터(Iterator)는 현재 어디에 있고, 다음엔 어디로 가는지 알 수 있다. 이는 메모리 차원에서 이점이 있다.
value, done, next() 을 이용하여 더 쓸모있는 동작이 가능한 객체를 만든다.

const cities = ['부산', '대구', '대전', '서울'];

const iter = cities.values();  
//또는  const iter = cities[Symbol.iterator]();

iter.next(); // {value: '부산', done: false}
// ... 
iter.next(); // {value: '서울', done: false}
iter.next(); // {value: undefined, done: true}

for(const x of iter) { console.log(x); } 
// value 값(부산, 대구, 대전, 서울) for-of와 [...arr] 가능!
// ←→ index가 없어도, next()를 보유하고 있어 loop 가능!

for-of는 index가 없는데 어떻게 순서대로 loop를 돌지?

iterable 하다는 것은 index가 없이도 순서대로 loop를 돌 수 있다는 말이다.
즉, for...of, spread(...instance) 가능하다

인스턴스 값을 iterable 하게 하기 위해서 [Symbol.iterator]를 구현해보자

[Symbol.iterator]() → value, done, next()

#1 values() 메서드를 이용하는 간단한 방법

class Collection {
    #arr;
    constructor(...args) {
        this.#arr = Array.isArray(args[0]) ? args[0] : [...args];
    }
    [Symbol.iterator]() {
        return this.#arr.values();
    }

    iterator() {
        // 관례적으로 이터레이터 함수를 만듬
        return this[Symbol.iterator]();
    }
...
}

const stack = new Stack(3, 3, 4, 2);
const queue = new Queue([1, 2]);
console.log(stack);
console.log([...stack, ...queue]); // [ 3, 3, 4, 2 ] [ 1, 2 ]

for (const s of stack) console.log(s);
for (const q of queue) console.log(q);

const itStack = stack[Symbol.iterator]();
console.log(itStack.next()); // { value: 3, done: false }
console.log(itStack.next()); // { value: 3, done: false }
console.log(itStack.next()); // { value: 4, done: false }
console.log(itStack.next()); // { value: 2, done: false }
console.log(itStack.next()); // { value: undefined, done: true }

super class 인 Collection에 [Symbol.iterator]()를 구현하는 방법이다. return 값으로 this.#arr.values() 메서드를 사용해서 iterable 한 값을 리턴한다. iterator() 메서드도 구현하였는데 이는 관례적으로 만들어 놓는다고 한다.
하지만 출제자의 의도는 이것을 원하는 것이 아니었다. 제너레이터를 넘어가기 위한 과정을 만들어보고 싶었던 것이다.

#2 next() 메서드를 직접 구현해서 iterable 하게

    // #2 next 함수 직접구현하기
    [Symbol.iterator]() {
        let idx = -1;
        let done = false;
        return {
            next: () => {
                idx += 1;
                done ||= idx >= this.#arr.length;
                return { value: this.#arr[idx], done };
            },
        };
    }

클로저를 사용해 next() 메서드를 직접 구현한 것이다.
[Symbol.iterator]() 안에 idx 와 done 변수를 정의하고 이 함수 리턴 값에 객체로 next 함수를 정의한다. key 값으로 next를 갖고, next key 값의 value로는 익명 함수를 넣는다. idx는 함수가 실행될 때마다 1씩 증가한다. 근데 done에는 새로운 할당연산자가 있지 않은가?

Logical OR assignment (||=)

The logical OR assignment (x ||= y) operator only assigns if x is falsy.
사실 done = done || idx >= this._arr.length; 얘랑 같은건데 단축표현식이다.

아무튼 done은 done 이거나 idx가 this.#arr의 length보다 작을 때까지 false를 반환한다. idx가 this.#arr.lengh 보다 같거나 클 때 true를 반환하므로 next가 끝이난다.

최종적으로 next()메서드와 같은 형태로 출력해주기 위해 {} 객체안에 value: this.#arr[idx] 값을 출력해주고 done 은 프로퍼티 축약 표현으로 key값만 적어준다.

next: () => {
      idx += 1;
      return { value: this.#arr[idx], done: !this.#arr[idx] };
  },                

더 단축해서 표현하자면 return에 해당 값을 넣을 수 있다.
value 에는 위와 같이 적어주고, done 에는 !를 앞에 붙여 값이 있을 땐 계속 falsy한 값이 나오다가 this.#arr의 인덱스를 벗어나는 순간부터 undefined가 나오기 때문에 undefined는 falsy 한 값이라 !와 붙으면 true 가 출력된다.

결론

iterable 하지 않은 값을 iterable하게 만들어주는 것은 여러 의미가 있다. 여러 메서드를 쓸 수 있고, 메모리의 효율 차원에서도 도움이 된다. 사실 실무에서 iterator를 짤 일은 없다고 한다. 하지만 iterator를 알아야 다음에 나오는 generator 나 비동기를 이해하는 초석이 된다. 실무자 중에 iterator와 generator를 알지 못한다고 답한 비율이 80%라고 한다. 나는 20%에 들어갈 기회가 생겼다. 이 글을 읽은 당신도!

출처

SSAC 영등포 교육기관에서 풀스택 실무 프로젝트 과정을 수강하고 있다. JS전반을 깊이 있게 배우고 실무에 사용되는 프로젝트를 다룬다. 앞으로 그 과정 중의 내용을 블로그에 다루고자 한다.

profile
트렌디 풀스택 개발자

0개의 댓글