ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • JS 문득 궁금한거 생길때 정리
    간단기법 2023. 6. 2. 04:03

    for ... of 

    for ... of 문은 iterable object에서 순차적으로 데이터에 대하여 연산을 실행한다. iterable object에는 Array, String, NodeList 등이 존재하며 arguments 오브젝트와 제너레이터 함수로 생성한 제너레이터 오브젝트도 해당한다. 아래 코드는 제너레이터로 생성한 오브젝트를 이용하여 for ... of 문을 실행하고 있다.

    const log = console.log;
    
    function* filter(f, list) {
      for (const a of list) {
        if(f(a))
          yield a;
      }
    }
    
    for(const a of filter(a => a%2, [1,2,3,4,5])) {
      log(a);
    }

    iterable object

    iterable object는 iterable protocol을 만족하는 오브젝트이다. 반복을 하려는 용도인데 기존의 반복문과 비교해 보면 그 특징을 알 수 있다. 보통의 반복문은 다음과 같을 것이다.

    for(let i = 0; i < array.length; i++) {
    	//array[i]에 하고싶은연산
    }

    이 반복문에선 array라는 배열처럼 생긴 데이터를 for문을 이용하여 순회하고 있다. 만약 array가 아니라 단순한 데이터 덩어리라도, length라는 프로퍼티와 인덱스 0, 1... 프로퍼티가 존재한다면 for문을 통하여 반복적인 연산이 가능하다. 하지만 본질적으로 for문과 이 데이터 덩어리는 분리되어 구별된다. 

    iterable object는 `데이터 + 반복가능`을 객체로 만들어서 다루는 것이다. 이를 위해 내부에 next메서드와 done, value라는 프로퍼티를 정의하는 [Symbol.iterator]를 갖는다. 간단하게 이터레이터라고 하는데, iterable object는 이터레이터를 갖는 혹은 iterable protocol을 구현하는 오브젝트라고 표현되는 것이다.

    const iterableObject = {
      data: ['apple', 'banana', 'cherry'],
      [Symbol.iterator]() {
        let index = 0;
        return {
          next: () => {
            if (index < this.data.length) {
              return {
                value: this.data[index++],
                done: false
              };
            } else {
              return { done: true };
            }
          }
        };
      }
    };
    
    for (const item of iterableObject) {
      console.log(item);
    }
    // 'apple'
    // 'banana'
    // 'cherry'

    다시 말하면, iterable object는 iterable protocol을 구현한 오브젝트로 내부에 [Symbol.iterator]를 갖는다. 이터레이터는 iterable object안에서 이터레이션을 책임지는 객체라고 볼 수 있다. 어떠한 오브젝트는 iterable object이면서 이터레이터이기도 하다.

     

    이러한 iterable object에는 Array, String, NodeList 등이 존재하며 arguments 오브젝트와 제너레이터 함수로 생성한 제너레이터 오브젝트를 포함한다.

    generator

    제너레이터는 제너레이터 함수와 제네레이터 함수가 리턴하는 제너레이터 오브젝트를 가리킨다. 제너레이터 오브젝트는 그 자체로 iterable object이면서 동시에 이터레이터이기도 하다. iterable object는 iterable protocol을 제공하는 오브젝트이고, iterator는 iterable object안에서 iterable protocol을 구현한 객체이다. 예를 들어, Array는 iterable object이지만 이터레이터는 아니다. 제너레이터는 iterable object이면서 이터레이터이다. 그 차이를 보자.

    // Array: iterable object(o) 이터레이터(x)
    [1,2,3].next(); // TypeError: [1,2,3].next is not a function
    
    for(const a of [1, 2, 3]) {
      log(a); // 1, 2, 3
    }
    
    // Generator iterable object(o) 이터레이터(o)
    function* filter(f, list) {
      for (const a of list) {
        if(f(a))
          yield a;
      }
    }
    
    filter(a => a%2, [1,2]).next(); // { value: 1, done: false }
    for(const a of filter(a => a%2, [1,2,3,4,5])) {
      log(a); // 1, 3, 5
    }

    Array는 iterable object으로 for .. of 문의 인자로 쓸 수 있다. 하지만 next() 메소드를 호출할 수는 없다.

    제너레이터 오브젝트는 for .. of 문의 인자로 쓸 수 있으며, 동시에 next() 메소드를 호출할 수 있다.

    array-like object

    array-like object는 배열 비스무리한 오브젝트이다. 배열은 아니지만 배열처럼 사용할 수 있는 오브젝트이다. 이 오브젝트의 특징은 length 프로퍼티가 있고 0번 인덱스부터 접근가능하다는 점이다. 하지만 역시 배열은 아니다.

    const arrayLike = {
      0: 'first',
      1: 'second',
      2: 'third',
      length: 3
    };
    
    for(let i = 0; i < arrayLike.length; i++) {
      log(arrayLike[i]); // first, second, third
    }
    
    for(const a of arrayLike) { // TypeError: arrayLike is not iterable
      log(a);
    }
    
    arrayLike.forEach(a => log(a)); // TypeError: arrayLike.forEach is not a function

    위 코드를 보면 알 수 있듯이, array-like 오브젝트는 length 프로퍼티와 0번부터 시작하는 인덱스를 갖는다. 그래서 일반적인 for문을 통하여 접근이 가능하다. 하지만, for .. of 문이나 forEach같은 배열 메소드는 사용할 수 없다.

    arguments

    arguments 오브젝트는 array like objects이다. 배열이랑 비슷하지만 배열은 아니다. length 프로퍼티가 있고, 0번째 인덱스부터 접근하는 특징이 있지만, 배열처럼 built-in 메소드가 없다. 대신 iterator는 있어서 for .. of 의 인자로 사용할 수 있다.

    arguments -> 배열
    const obj = [...arguments];
    cosnt obj = Array.from(arguments);
    
    arguments는 iterable object이기도 하다.
    function abc(a, b, c) {
      for(const a of arguments) {
        log(a);
      }
    }
    
    abc(1, 2, 3); // 1, 2, 3

    for ... in

    for ... in 문은 오브젝트의 enumerable string 프로퍼티를 순회하는 반복문이다. 이때, 오브젝트가 enumerable한 프로퍼티를 상속받았다면, 해당 프로퍼티도 순회의 대상이 된다.

     

    자바스크립트 오브젝트의 프로퍼티에는 숨겨진 속성들이 있다. enumerable, configuable, writable, value이다. 

    const obj = {
    	a: 1;
    }
    
    const obj = {
    	a: {
        	configuable: true or false,
            writable: true or false,
            enumerable: true or false,
            value: 1,
        }
    }

    어떤 프로퍼티이든 숨겨진 속성이 있고, 그 속성중에 enumerable이라는 것이 있는 것이다. 따라서 for ... in 문의 enumerable이라는 의미는 enumerable 속성이 true로 되어있는 것을 가리킨다.

     

    enumerable string 프로퍼티에서 남은것은 string인데, 이것은 symbol이 아닌 프로퍼티를 가리킨다. 예를 들어 프로퍼티가숫자인 0이라도, symbol이 아니므로 대상에 해당된다.

    const objEnumerable = {
      0: "abcd",
      "a": 10
    };
    
    for(const property in objEnumerable) {
      log(a); // '0' 'a'
      log(objEnumerable[property]) // 'abcd', 10
    }

    for ... in vs for ... of

    for ... in 문의 대상이 되는 것은 enumerable string이고

    for ... of 문의 대상이 되는 것은 iterable object이다.

     

    다음 코드로 차이를 알아보자.

    const arr = [1,2,3,4];
    
    arr[100] = 1;
    for(const key in arr) {
      console.log(key);
    }
    
    for(const item of a) {
      console.log(item);
    }

    array의 index는 enumerable 프로퍼티로 정수를 이름으로 쓰는 것만 다를뿐 다른 일반적인 오브젝트의 프로퍼티와 동일한다. for ... in으로 쓴다면,  1, 2, 3, 4, 100이 출력될 것이다.

     

    array는 iterable object이므로 for ... of로 사용할 수 있다. 이때, 출력되는 것은 1,2,3,4 이후로 undefined가 90여개 나오고 그후로 1이 나온다.

    2차원 배열

    javascript로 2차원 배열을 만드는 코드를 생각해 보자.

    const row = 5;
    const col = 3;
    
    Array(row).fill(new Array(col));

    위 코드엔 문제점이 있다. 모든 행이 똑같아지는 것이다. fill 메소드에 new Array(col)를 그대로 넘기면, Array(row)의 모든 행이 하나의 배열을 바라보게 된다. 위 코드를 풀어 쓰면 더 잘 보인다.

    const row = 5;
    const col = 3;
    
    const temp = new Array(col);
    Array(row).fill(temp);

    이를 막기 위해서 fill 메소드에 배열을 넘기지 말고, map을 이용하여 생성해 주면 된다.

    const arr = new Array(row).fill(0).map(_=> new Array(col).fill(0);

    그런데, 만약 정수 대신 객체를 넣는다고 하면 어떨까?

    const arr = new Array(row).fill(0).map(_=> new Array(col).fill({value: 0, isVisted:false});

    역시 처음과 같은 문제가 발생한다. 이렇게 되면 row가 같은 원소들은 모두 하나의 객체를 바라보게 된다. arr[0][0~col-1]까지 하나의 객체를 바라보고, arr[1][0~co-1]까지 또 하나의 객체를 바라본다.

    const arr = new Array(row).fill(0).
    map(_=> new Array(col).fill(0).map(_ => {return {value: 0, isViisted:false}}));

    이를 해결하려면 map을 활용하면 된다.

    참고

    댓글

Designed by Tistory.