본문 바로가기

[JS] 비동기 흐름 : 배열 메서드 구현해보기 (Underbar) - 중

[JS] 비동기 흐름 : 배열 메서드 구현해보기 (Underbar) - 중

 

rest parameter를 자유자재로 사용하고, _.shffule에서 immutable 개념을 복습해본다.

 

구현해볼 배열 메서드

  • 함수 커스텀 메서드 : once, delay  
  • 배열 내장 메서드 : includes, every, some
  • 객체 커스텀 메서드 : extends, defaults
  • 배열 커스텀 메서드 : zip, zipStrict, intersection, difference, shuffle

 

함수 커스텀 메서드

'use strict';

/**
 * FUNCTIONS
 * =========
 * 클로저의 특징을 활용해 다양한 형태의 함수를 구현할 수 있다.
 * 이를 활용하여 기존 함수가 여러 번 실행되면 결과가 변동되는 함수를, 
 * 한 번 리턴된 값만 리턴하게 하는 함수(_.once)와
 * 기존 함수가 즉시 실행 되던 함수를, 일정 시간 이후에 실행되게 하는 함수(_.delay)로 만들어본다.
 *
 * 이는 일반적인 프로그래밍 디자인 패턴 중 데코레이터(또는 wrapper) 패턴과 유사하다.
 * 데코레이터 패턴은 객체를 꾸미거나(decorate) 감싸서(wrap) 기존 객체에 기능 또는 행동을 추가한다.
 *  이론: https://refactoring.guru/design-patterns/decorator
 *  구현: https://www.dofactory.com/javascript/design-patterns/decorator
 */


  _.once = function (func) {
  
  // 아래 변수들은 아래 선언/리턴되는 함수 안에서 참조된다.
  // 리턴되는 함수의 scope 내에 존재하므로, 리턴되는 함수를 언제 실행해도 이 변수들에 접근할 수 있다.
  let result;
  let alreadyCalled = false;

  return function (...args) {
    if (!alreadyCalled) {         // alreadyCalled 값이 false면 (초기 세팅)
      alreadyCalled = true;
      result = func(...args);     // arguments 키워드 혹은, spread operator를 사용
    }
    
    return result;
  };
};
// _.delay는 입력으로 전달되는 시간(ms, 밀리초)후 
// callback 함수를 함께 전달되는 (임의의 개수의) 인자와 함께 실행한다.
// 예를 들어, _.delay(func, 500, 'a', 'b')의 결과로 
// '최소' 500m가 지난 이후에 func('a', 'b')가 호출된다. (비동기)

_.delay = function (func, wait, ...args) {
  setTimeout(function () {
    func(...args);
  }, wait);
};

 

 

배열 내장 메서드 

/**
 * ARRAY METHODS
 * =============
 * 자바스크립트 내장 배열 메소드를 직접 구현해본다.
 */ 

// _.includes는 배열이 주어진 값을 포함하는지 확인한다.
// 일치 여부의 판단은 엄격한 동치 연산(strict equality, ===)을 사용해야 한다.
// 입력으로 전달되는 배열의 요소는 모두 primitive value라고 가정한다.

_.includes = function (arr, target) {
  let result = false;

  _.each(arr, function (item) {
    if (item === target) {
      result = true;
    }
  });
  return result;
};
// _.every는 배열의 모든 요소가 test 함수(iteratee)를 통과하면 true를, 그렇지 않은 경우 false를 리턴한다.
// test(element)의 결과(return 값)가 truthy일 경우, 통과한다.
// _.each를 반드시 사용할 필요는 없다.
// iteratee가 주어지지 않을 경우, 모든 요소가 truthy인지 확인한다. 
// 빈 배열을 입력받은 경우, true를 리턴한다. (공허하게 참, vacantly true)

_.every = function (arr, iteratee) {
  if (iteratee === undefined) {
    iteratee = _.identity;        //identity는 전달인자(argument)가 무엇이든, 그대로 리턴
  }

  for (let i = 0; i < arr.length; i++) {
    if (!iteratee(arr[i])) {    
      return false;
    }
  }
  return true;
};
// _.some은 배열의 요소 중 하나라도 test 함수(iteratee)를 통과하면 true를, 그렇지 않은 경우 false를 리턴한다. 
// 빈 배열을 입력받은 경우, false를 리턴한다. 
// 그 외 조건은 앞서 _.every와 동일하다. 
_.some = function (arr, iteratee) {
  if (iteratee === undefined) {
    iteratee = _.identity;
  }

  for (let i = 0; i < arr.length; i++) {
    if (iteratee(arr[i])) {
      return true;                        // 빠른 return
    }
  }
  return false;
};

 

객체 커스텀 메서드

/**
 * CUSTOM OBJECT METHODS
 * =====================
 * 자바스크립트 객체를 더 쉽게 다룰 수 있는 커스텀 객체 메소드를 직접 구현해본다. 
 */ 

// _.extend는 여러 개의 객체를 입력받아, 순서대로 객체를 결합한다.
// 첫 번째 입력인 객체를 기준으로 다음 순서의 객체들의 속성을 덮어쓴다.
// 최종적으로 (속성이 추가된) 첫 번째 객체를 리턴한다. (새로운 객체 X)

// 아래 예제를 참고 
//  let obj1 = { key1: 'something' };
//  _.extend(
//    obj1,
//    {
//      key2: 'something new',
//      key3: 'something else new'
//    },
//    {
//      blah: 'even more stuff',
//      key3: 'overwrite"
//    }
//  );
// console.log(Object.keys(obj1)) // --> key1, key2, key3, blah
// console.log(obj1.key3) // --> 'overwrite"

// _.extend로 전달되는 객체의 수는 정해져 있지 않다.
// spread syntax 또는 arguments 객체를 사용해야 한다.
// 함수의 시그니쳐(함수의 입력과 출력, 함수의 모양)를 적절하게 변형하여 사용한다.  
// _.each를 사용해서 구현한다.

// (아래 함수 설명) ...rest는 {},{} 등을 넣은 형태로 [{},{}]로 전달
// 예를 들어 위의 예시대로라면 
// _.extend = function (obj1, [{key2: 'something new', key3: 'something else new'}, {..}]) {..} 형태가 된다.

_.extend = function (base, ...rest) {
 _.each(rest, function (item) {        // 예) 배열의 0번째 요소(Item 즉, 객체 {key2: 'something new', key3: 'something else new',})
  
    _.each(item, function (val, key) { // 예) 객체 {key2: 'something new', key3: 'something else new',}의 값, 키
      base[key] = val;                 // base인 obj1에 해당 key와 val을 넣는다.
      
    });
  });
  return base;
};
// _.defaults는 _.extend와 비슷하게 동작하지만, 이미 존재하는 속성(key)을 덮어쓰지 않는다.

_.defaults = function (base, ...rest) {
  _.each(rest, function (item) {            // 여기까지는 똑같다. (예시를 위와 동일하게 쓰겠다.)
  
    const keys = Object.keys(base);         // keys = [ obj1의 키 값이 배열 형태로 나열된다. ]
    _.each(item, function (val, key) {      // 똑같이 예) 객체 {key2: 'something new', key3: 'something else new',}의 값, 키
      if (base[key] === undefined) {        // obj1에 뒤에 나오는 객체 속 키 값이 없으면!! **
      base[key] = val;                      // base인 obj1에 해당 key와 val을 넣는다.
      }
    });
  });
  return base;           
};

 

배열 커스텀 메서드

/**
 * CUSTOM ARRAY METHODS
 * ====================
 * 자바스크립트 배열을 더 쉽게 다룰 수 있는 배열 커스텀 메소드를 직접 구현해본다. 
 */ 

// _.zip은 여러 개의 배열을 입력받아, 같은 index의 요소들을 묶어 배열로 만든다.
// 각 index 마다 하나의 배열을 만들고, 최종적으로 이 배열들을 요소로 갖는 배열을 리턴한다.
// _.zip의 입력으로 전달되는 배열이 수는 정해져 있지 않고, 각 배열의 길이는 다를 수 있다.
// 최종적으로 리턴되는 배열의 각 요소의 길이는 입력으로 전달되는 배열 중 가장 '긴' 배열의 길이로 통일된다.
// 특정 index에 요소가 없는 경우, undefined를 사용한다.
// 반복문(for, while)을 사용할 수 있다.
// _.each, _.reduce, _.pluck 중 하나 이상을 반드시 사용하여야 한다.

// 예시
//  const arr1 = ['a','b','c'];
//  const arr2 = [1,2];
//  const result = _.zip(arr1, arr2)
//  console.log(result); // --> [['a',1], ['b',2], ['c', undefined]]

_.zip = function (...args) {     // args는 배열 속 배열 형태
  const maxLength = _.reduce(    // reduce에 배열 속 배열들이 몽땅 들어간다. 초기값은 0이다.
    args,
    function (max, item) {       // 초기값 0과, 현재값 비교 (첫 현재값은 예시로 치면 arr2 배열)  
      if (max < item.length) {     
        return item.length;
      }
      return max;           // 배열 속 배열들을 다 돌면서 가장 배열의 길이가 긴 배열의 길이값 리턴
    },
    0
  );

  const result = [];
  for (let i = 0; i < maxLength; i++) { // 가장 길이가 긴 배열 만큼 반복
    const grouped = _.pluck(args, i);   // pluck에도 배열 속 배열들을 몽땅 넣는다. 각 배열들의 i번째 요소들 모음
    result.push(grouped);               // 모은 것을 추출한 배열 하나를 result 배열에 넣는다.
  }

  return result;
};
// _.zipStrict은 _.zip과 비슷하게 동작하지만,
// 최종적으로 리턴되는 배열의 각 요소의 길이는 입력으로 전달되는 배열 중 가장 '짧은' 배열의 길이로 통일된다.
// 그 외 조건은 앞서 _.zip과 동일하다.
_.zipStrict = function (...args) {
  const minLength = _.reduce(
    args,
    function (min, item) {
      if (item.length < min) {
        return item.length;
      }
      return min;
    },
    Number.MAX_SAFE_INTEGER   //  해당 상수는 JS에서 안전한 최대 정수값을 나타낸다. (2^53 - 1).
  );

  const result = [];
  for (let i = 0; i < minLength; i++) {
    const grouped = _.pluck(args, i);
    result.push(grouped);
  }
  return result;
};
// _.intersection은 여러 개의 배열을 입력받아, 교집합 배열을 리턴한다.
// 교집합 배열은 모든 배열에 공통으로 등장하는 요소들만을 요소로 갖는 배열이다.
// 교집합 배열의 요소들은 첫 번째 입력인 배열을 기준으로 한다.
// 교집합이 없는 경우 빈 배열을 리턴한다.

// 예시 (함수 설명에도 적용해본다.)
//  const set1 = ['a', 'e', b', 'c'];
//  const set2 = ['c', 'd', 'e'];
//  const result = _.intersection(set1, set2);
//  console.log(result) // --> ['e', 'c']
//                      // 첫 번째 배열에 'e'가 먼저 등장

_.intersection = function (base, ...rest) {  // base가 되는 set1과 [set2]형태로 전달
  let result = [];
  _.each(base, function (bItem) {        // set1 배열을 하나하나 살펴본다. 0번째 bItem은 'a'
    const intersected = _.every(rest, function (arr) { // 배열 속 배열이 들어간다. arr은 set2 배열
      return _.includes(arr, bItem);  // set2배열 요소에 bItem이 있으면 true 반환 
    });

    if (intersected) {           // 만약에 set2배열 요소에 bItem이 있으면
      result.push(bItem);        // result에 넣는다. (base 기준이므로 첫 번째 배열 기준) 
    }
  });
  return result;

  // ** 다른 방법 : 차례대로 두 배열의 교집합을 구하는 방법 // ??
  // 
  // return _.reduce(                     // reduce에 배열 속 배열을 넣음, 초기값 base 배열
  //   rest,
  //   function (intersected, arr) {      // reduce 함수에 intersected와 배열 1개가 전달됨
  //     return _.filter(intersected, function (ele) {    
  //       return _.includes(arr, ele);                 
  //     });
  //   },
  //   base
  // );
};
// _.difference는 여러 개의 배열을 입력받아, 차집합 배열을 리턴한다.
// 차집합 배열은 첫 번째 배열에서 차례대로 다음 배열들의 요소들을 제외한 배열이다.
// 차집합 배열의 요소들은 첫 번째 입력인 배열을 기준으로 한다.
// 차집합이 없는 경우 빈 배열을 리턴한다.

// 예시
//  const set1 = ['a', 'b', 'c'];
//  const set2 = ['b', 'c', 'd'];
//  const result = _.difference(set1, set2);
//  console.log(result) // --> ['a']

_.difference = function (base, ...rest) {                 // ...rest는 배열 속 배열들 형태
  let result = [];
  _.each(base, function (bItem) {                         // set1의 0번째 'a'가 들어갈 때
    const existSomewhere = _.some(rest, function (arr) {  // includes에서 하나라도 true가 나오면 true 리턴
      return _.includes(arr, bItem);                      // 0번째 배열(set2)에 'a'가 있으면 true
    });

    if (!existSomewhere) {        // existSomewhere이 false일 때(중복 값이 하나도 없을 때)
      result.push(bItem);         // base의 요소를 새 배열 속에 넣어준다.
    }
  });
  return result;

  // 다른 방법 : 차례대로 두 배열의 차집합을 구하는 방법 //??
  // return _.reduce(
  //   rest,
  //   function (intersected, arr) {
  //     return _.filter(intersected, function (ele) {
  //       return !_.includes(arr, ele);
  //     });
  //   },
  //   base
  // );
};

 

Array.prototype.sort 요약

compareFunction이 제공되면 배열 요소는 compare 함수의 반환 값에 따라 정렬된다. a와 b가 비교되는 두 요소라면,

  • compareFunction(a, b)이 0보다 작은 경우a를 b보다 낮은 색인으로 정렬한다. 즉, a가 먼저온다.
    -> (음수면 첫 번째 인자부터 정렬)
  • compareFunction(a, b)이 0을 반환하면 보통 a와 b를 서로에 대해 변경하지 않고 모든 다른 요소에 대해 정렬한다. 
  • compareFunction(a, b)이 0보다 큰 경우, b를 a보다 낮은 인덱스로 소트한다.
예시)

var numbers = [4, 2, 5, 1, 3];

numbers.sort(function(a, b) { return a - b; });    // 4와 2를 비교하면 4-2는 양수, 2를 4보다 낮은 인덱스로 정렬
console.log(numbers); // [1, 2, 3, 4, 5]
// _.sortBy는 배열의 각 요소에 함수 transform을 적용하여 얻은 결과를 기준으로 정렬한다.
// transform이 전달되지 않은 경우, 배열의 요소 값 자체에 대한 비교 연산자의 결과를 따른다.
// 예를 들어, number 타입간 비교는 대소 비교이고 string 타입간 비교는 사전식(lexical) 비교이다.
// 세 번째 인자인 order는 정렬의 방향을 나타낸다. 생략되거나 1을 입력받은 경우 오름차순, -1을 입력받은 경우 내림차순으로 정렬한다.

// 예시
//  const people = [
//    { id: 1, age: 27 },
//    { id: 2, age: 24 },
//    { id: 3, age: 26 },
//  ];
//  function byAge(obj) {
//    return obj.age;
//  };
//  const result = _.sortBy(people, byAge);
//  console.log(result); // --> [{ id: 2, age: 24 }, { id: 3, age: 26 }, { id: 1, age: 27 }]

// 한편, 'undefined'는 비교 연산은 가능하지만 사실 비교가 불가능한(비교의 의미가 없는) 데이터이다.

// 예시
//  console.log(undefined > 0); // --> false
//  console.log(undefined < 0); // --> false
//  console.log(undefined == 0); // --> false
//  console.log(undefined === 0); // --> false
//  console.log(undefined > 'hello'); // --> false
//  console.log(undefined < 'hello'); // --> false

// 이러한 이유로 정렬하려는 데이터들 중 'undefined'가 있는 경우,
//  1) 'undefined' 값을 제외(filter)하고 비교하거나
//  2) 'undefined' 값을 어떤 다른 값으로 간주하여 비교해야 한다. ** 이 방식으로 적용


// 힌트
//  1. Array.prototype.sort를 사용할 수 있다.
//    참고 문서: https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Array/sort
//    (예제를 통해 내장 메소드 sort의 특성을 꼭 확인)
//  2. _.identity를 사용할 수 있다.

//  학습 우선순위: bubble sort, insertion sort, quick sort, merge sort, radix sort

_.sortBy = function (arr, transform, order) {
  order = order || 1;                       // 입력받은 값이 없으면 오름차순(1)
  transform = transform || _.identity;      // 값을 변형할 함수 입력이 없으면 값 자체 비교
  
  const arrCloned = _.slice(arr);
  
  return arrCloned.sort(function (a, b) {   // a와 b를 비교했을 때
    if (transform(a) < transform(b)) {      // 콜백 함수로 값이 변형된 값이 a가 작으면
      return -1 * order;                    // order 값 음수 or 양수로 바꿈 
    }                    // 만약 order이 -1로 큰 수부터 정렬된다면 +1로 바뀐 후 a보다 큰 수인 b가 먼저 오게 된다.  
    return order;                           // 그게 아니면 order 값 그대로 
  });
};
// _.shuffle은 배열 요소의 순서가 랜덤하게 변경된 새로운 배열을 리턴한다.
// 다양한 상황(예. 비디오 또는 음악 재생의 순서를 섞을 때)에서 유용하게 쓰일 수 있다.
// _.shuffle의 동작을 이해하는 것이 목적이므로, 구현할 필요는 없다.
// 아래 사이트에서 테스트 해볼 수 있다.
//  https://bost.ocks.org/mike/shuffle/compare.html
// (단, 해당 사이트의 shuffle 함수는 입력으로 전달되는 array의 요소들의 위치를 '직접' 변경해야 한다.)


_.shuffle = function (arr) {
  let arrCloned = arr.slice();
  for (let fromIdx = 0; fromIdx < arr.length; fromIdx++) {
    const toIdx = Math.floor(Math.random() * arr.length); // 배열 길이가 6이면 5까지의 랜덤 값을 뱉는다
    // 아래 코드는 두 변수의 값을 교환한다.
    let temp = arrCloned[fromIdx];            // c = a (a 값을 c에 임시로 할당)
    arrCloned[fromIdx] = arrCloned[toIdx];    // a = b (b 값을 a에 주고)
    arrCloned[toIdx] = temp;                  // b = c (c에 담긴 a 값을 b에게 준다)
  }
  return arrCloned;
};

 


 

오키! 일주일 전의 나와 다르다! 그때는 막막했는데 지금은 다 이해가 된다!!!! 

역시 어제보다는 오늘이, 조금씩 성장해 가는 구나 🤍

728x90
⬆︎