[TSR] Underbar 기능 구현

EllaDev·2022년 1월 18일
0

today-study-record

목록 보기
1/8
post-thumbnail

해당글은 오늘 공부한 내용을 정리하여 메모하는 형식으로 작성한 것으로
설명이나 이해를 돕는 글이 아님을 명시합니다.


Underbar 기능 구현

배열이나 객체를 다루기 위한 간단한 Underbar 라이브러리 기능을 구현한다.
underscore.js, lodash등과 같은 라이브러리를 모티브로 JS의 내장메소드의 기능을 구현해본다.

 let _ = {}
  • 각 기능을 구현한 함수를 가져다가 쓸 수 있다.

slice 기능

_.slice = function (arr, start, end) {
  let _start = start || 0
  let _end = end;

  if (start < 0) _start = Math.max(0, arr.length + start);
  if (end < 0) _end = Math.max(0, arr.length + end);

  if (_end === undefined || _end > arr.length) _end = arr.length;

  let result = [];
  
  for (let i = _start; i < _end; i++) {
    result.push(arr[i]);
  }

  return result;
};
  • start가 undefined인 경우, slice는 0부터 동작
  • 입력받은 인덱스가 음수일 경우, 마지막 인덱스부터 매칭한다.
  • 입력받은 인덱스는 0 이상이어야 한다.
  • end가 생략될 경우(undefined), slice는 마지막 인덱스까지 동작한다.
  • end가 배열의 범위를 벗어날 경우, slice는 마지막 인덱스까지 동작한다.
  • start가 배열의 범위를 벗어날 경우, 빈 배열을 리턴한다.

take 기능

_.take = function (arr, n) {
  if (n === undefined || n < 0) {
    return [];
  }
  if (n > arr.length) {
    return [...arr];
  }
  let result = _.slice(arr, 0, n);
  return result;
};
  • 기능: 배열의 처음 n개의 element를 담은 새로운 배열을 리턴한다.
  • n이 undefined이거나 음수인 경우, 빈 배열을 리턴한다.
  • n이 배열의 길이를 벗어날 경우, 전체 배열을 shallow copy한 새로운 배열을 리턴한다.

Spread Syntax 사용

  • 얕은 복사인 let copy = arr를 사용하려고 했는데 코드의 가독성을 위해서 spread syntax 사용했다.
  • 여기서 let copy = arr[...array]의 복사 방식이 다르지 않을까 생각하여 찾아보다가 배열의 깊은 복사와 얕은 복사에 대해서 정리가 필요하다고 느껴졌다.

drop 기능

_.drop = function (arr, n) {
  if (n > arr.length) {
    return [];
  }
  if (n === undefined || n < 0) {
    return [...arr];
  }

  let result = _.slice(arr, n);
  return result;
};
  • 기능: 처음 n개의 element를 제외한 새로운 배열을 리턴한다.
  • n이 undefined이거나 음수인 경우, 전체 배열을 shallow copy한 새로운 배열을 리턴한다.
  • n이 배열의 길이를 벗어날 경우, 빈 배열을 리턴합니다.

last 기능

_.last = function (arr, n) {
  let length = arr.length;
  if (n === undefined || n < 0) {
    return [arr[length - 1]];
  }
  if (n > arr.length) {
    return [...arr];
  }
  let result = _.drop(arr, length - n);
  return result;
};
  • 기능 : 배열의 마지막 n개의 element를 담은 새로운 배열을 리턴한다.
  • n이 undefined이거나 음수인 경우, 배열의 마지막 요소만을 담은 배열을 리턴한다.
  • n이 배열의 길이를 벗어날 경우, 전체 배열을 shallow copy한 새로운 배열을 리턴한다.
  • _.drop 활용

each 기능

_.each = function (collection, iteratee) {
  if (Array.isArray(collection)) {
    for (let i = 0; i < collection.length; i++) {
      iteratee(collection[i], i, collection);
    }
  } else {
    for (let item in collection) {
      iteratee(collection[item], item, collection);
    }
  }
};
  • 기능 : collection의 각 데이터에 반복적인 작업을 수행한다.
    1. collection(배열 혹은 객체)과 함수 iteratee(반복되는 작업)를
      인자로 전달받아 (iteratee는 함수의 인자로 전달되는 함수이므로 callback 함수)
    2. collection의 데이터(element 또는 property)를 순회하면서
    3. iteratee에 각 데이터를 인자로 전달하여 실행한다.
  • iteratee는 매게변수로 차례로 데이터(element 또는 value), 접근자(index 또는 key), collection을 가진다.
  • _.each는 명시적으로 어떤 값을 리턴하지 않는다.

indexOf 기능

_.indexOf = function (arr, target) {
  // 배열의 모든 요소에 접근하려면, 순회 알고리즘(iteration algorithm)을 구현해야 한다.
  // 반복문을 사용하는 것이 가장 일반적이지만, 지금부터는 이미 구현한 _.each 함수를 활용하여야 한다.
  let result = -1;

  _.each(arr, function (item, index) {
    if (item === target && result === -1) {
      result = index;
    }
  });

  return result;
};
  • 기능: target으로 전달되는 값이 arr의 요소인 경우, 배열에서의 위치(index)를 리턴한다.그렇지 않은 경우, -1을 리턴한다.
  • target이 중복해서 존재하는 경우, 가장 낮은 index를 리턴한다.

filter 기능

_.filter = function (arr, test) {
  // TODO: 여기에 코드를 작성한다.
  // arr를 순회하면서 test 함수가 true인 요소 찾기
  // 그 요소들의 새로운 배열 리턴
  let result = [];
  _.each(arr, function (item) {
    if (test(item)) {
      result.push(item);
    }
  });
  return result;
};
  • 기능 : test 함수를 통과하는 모든 요소를 담은 새로운 배열을 리턴한다.
  • test(element)의 결과(return 값)가 truthy일 경우, 통과이다.
  • test 함수는 각 요소에 반복 적용된다.

reject 기능

_.reject = function (arr, test) {
  // TODO: 여기에 코드를 작성한다.
  // TIP: 위에서 구현한 `filter` 함수를 사용해서 `reject` 함수를 구현해 보세요.
  let result = [];
  _.each(arr, function (item) {
    if (!test(item)) {
      result.push(item);
    }
  });
  return result;
};
  • 기능 : _.filter와 정반대로 test 함수를 통과하지 않는 모든 요소를 담은 새로운 배열을 리턴한다.

uniq 기능

_.uniq = function (arr) {
  // TODO: 여기에 코드를 작성한다.
  // arr를 순회하면서 item이 다른 item과 같은게 있는지 확인
  // 같은게 있다면 그냥 넘어감
  // 없으면 result에 추가
  let result = [];
  for (let i = 0; i < arr.length; i++) {
    let diffIndex = _.indexOf(arr, arr[i]);
    if (diffIndex === i) {
      result.push(arr[i]);
    }
  }
  return result;
};
  • 기능 : 주어진 배열의 요소가 중복되지 않도록 새로운 배열을 리턴한다.
  • 중복 여부의 판단은 엄격한 동치 연산(strict equality, ===)을 사용해야 한다.
  • 입력으로 전달되는 배열의 요소는 모두 primitive value라고 가정한다.

map 기능

_.map = function (arr, iteratee) {
  // _.map 함수는 매우 자주 사용됩니다.
  // _.each 함수와 비슷하게 동작하지만, 각 요소에 iteratee를 적용한 결과를 리턴한다.

  // arr를 순회하면서 각 요소를 iteratee에 적용한다.
  // 적용된 요소를 새배열로 리턴

  let result = [];
  _.each(arr, function (item, index) {
    result.push(iteratee(item, index, arr));
  });
  return result;
};
  • 기능 : iteratee(반복되는 작업)를 배열의 각 요소에 적용(apply)한 결과를 담은 새로운 배열을 리턴한다.
  • 함수의 이름에서 드러나듯이 _.map은 배열의 각 요소를 다른 것(iteratee의 결과)으로 매핑(mapping)한다.
  • _.each 함수 사용

pluck 기능

_.pluck = function (arr, keyOrIdx) {
  // _.pluck을 _.each를 사용해 구현하면 아래와 같습니다.
  // let result = [];
  // _.each(arr, function (item) {
  //   result.push(item[keyOrIdx]);
  // });
  // return result;
  // _.pluck은 _.map을 사용해 구현
    // arr를 순회하면서 keyOrIdx를 가지는 요소값 추출
    // 없으면 result에 undefined를 push
    // 있으면 result에 push
  let result = _.map(arr, function (item, index) {
    if (item[keyOrIdx]) {
      return item[keyOrIdx];
    } else {
      return undefined;
    }
  });
  return result;
};
  • 기능 :
      1. 객체 또는 배열을 요소로 갖는 배열과 각 요소에서 찾고자 하는 key 또는 index를 입력받아
      1. 각 요소의 해당 값 또는 요소만을 추출하여 새로운 배열에 저장하고,
      1. 최종적으로 새로운 배열을 리턴한다.
  • 최종적으로 리턴되는 새로운 배열의 길이는 입력으로 전달되는 배열의 길이와 같아야 한다.
  • key 또는 index를 가지고 있지 않은 요소의 경우, 추출 결과는 undefined 이다.

reduce 기능

_.reduce = function (arr, iteratee, initVal) {
  //  배열을 순회하며 각 요소에 iteratee 함수를 적용하고,
    //  iteratee 초기값 여부
      // iteratee 초기값 O => initVal 적용 및 arr 그대로
      // iteratee 초기값 X => initVal = arr[0] 이고 arr[1] - arr[n] 반복
  //  그 결과값을 계속해서 누적(accumulate)한다.
  //  최종적으로 누적된 결과값을 리턴한다.
  let acc;
  if (initVal === undefined) {
    acc = arr[0];
    let newArray = _.slice(arr, 1);
    _.each(newArray, function (item, index) {
      acc = iteratee(acc, item, index, arr);
    });
  } else {
    acc = initVal;
    _.each(arr, function (item, index) {
      acc = iteratee(acc, item, index, arr);
    });
  }
  return acc;
};
  • 기능
      1. 배열을 순회하며 각 요소에 iteratee 함수를 적용하고,
      1. 그 결과값을 계속해서 누적(accumulate)한다.
      1. 최종적으로 누적된 결과값을 리턴한다.
  • iteratee는 매게변수로 데이터(element 또는 value), 접근자(index 또는 key), collection을 가진다.

초기값 설정과 누적값 처리에 대해 고민

once 기능

_.once = function (func) {
  // 이 함수가 처음 호출됐는지를 확인
  // 처음이면 콜백 함수를 정상적으로 호출하고
  // 만약 처음 호출이 아니면 이전 값을 리턴한다.
  let called = false;
  let result;

  return function () {
    // TIP: arguments 키워드 혹은, spread operator를 사용하세요.
    if (!called) {
      result = func(...arguments);
    }
    called = true;
    return result;
  };
};
  • 기능 : callback 함수를 한 번만 호출하는 '함수'를 리턴한다.
  • _.once가 리턴하는 함수를 여러 번 호출해도 callback 함수는 한 번 이상 호출되지 않습니다.

클로저

  • 정의: 함수와 함수가 선언된 어휘적 환경(lexical environment)의 조합
  • 특징: 내부 함수가 외부 함수 안에 선언된 변수에 접근할 수 있다. 내부 함수를 클로저 함수라고 부르기도 한다.
  • 응용: namespacing, privacy, function factory, partially applied functions

여기서 고차함수, 클로저함수, 콜백함수, 커링 함수의 차이점에 대해서 페어와 이야기를 나누게 되었고 이에 대한 내용은 따로 포스팅을 할 예정이다.

delay 기능

_.delay = function (func, wait, ...value) {
  
  setTimeout(() => func(...value), wait);
};
  • 기능 : 입력으로 전달되는 시간(ms, 밀리초)후 callback 함수를 함께 전달되는 (임의의 개수의) 인자와 함께 실행한다.

자바스크립트 내장 배열 메소드를 직접 구현

includes 기능

_.includes = function (arr, target) {
  // 반복문을 돌면서 각 요소들을 target과 비교한다.
  // 일치하면 true 리턴하고
  // 반복문 끝까지 일치하는게 없으면 false를 리턴한다.

  let result = false;
  _.each(arr, (element) => {
    if (element === target) {
      result = true;
    }
  });

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

primitive value

every 기능

_.every = function (arr, iteratee) {
  //빈 배열을 입력받은 경우
  // true를 리턴
  if (!arr.length) {
    return true;
  }
  //iteratee가 주어졌는지 확인
  if (iteratee === undefined) {
    //iteratee가 없으면 각 요소가 truthy인지 확인
    for (const element of arr) {
      if (!element) {
        return false;
      }
    }
  } else {
    //반복문 돌면서 각 요소에 iteratee 를 적용해서
    // 모든 요소가 true면 true를 return
    // 하나라도 false면 false를 return
    for (const element of arr) {
      if (!iteratee(element)) {
        return false;
      }
    }
  }
  return true;
};
  • 기능 : 배열의 모든 요소가 test 함수(iteratee)를 통과하면 true를, 그렇지 않은 경우 false를 리턴한다.
  • test(element)의 결과(return 값)가 truthy일 경우, 통과입니다.
  • iteratee가 주어지지 않을 경우, 모든 요소가 truthy인지 확인한다.
  • 빈 배열을 입력받은 경우, true를 리턴해야 한다. (공허하게 참, vacantly true)

some 기능

_.some = function (arr, iteratee) {
  // TODO: 여기에 코드를 작성한다.

  // 빈 배열인 경우에 false를 리턴
  if (!arr.length) {
    return false;
  }

  //iteratee가 주어졌는지 확인
  if (iteratee === undefined) {
    //iteratee가 없으면 배열의 요소 중 하나라도 truthy이면 true를 return
    for (const element of arr) {
      if (element) {
        return true;
      }
    }
  } else {
    //반복문을 돌면서 배열의 요소 중 하나라도 true면 true를 return
    //그 외는 false를 return
    for (const element of arr) {
      if (iteratee(element)) {
        return true;
      }
    }
  }
  return false;
};
  • 기능 : 배열의 요소 중 하나라도 test 함수(iteratee)를 통과하면 true를, 그렇지 않은 경우 false를 리턴한다.
  • 빈 배열을 입력받은 경우, false를 리턴해야 한다.
  • 그 외 조건은 앞서 _.every와 동일한다.

자바스크립트 객체를 더 쉽게 다룰 수 있는 커스텀 객체 메소드를 구현

extend 기능

_.extend = function (obj1, ...objs) {
  // objs = [obj1, obj2, obj3, ...]
  // 반복문을 돌면서(_.each) 첫번째 obj에 나머지 obj를 순서대로 추가해준다.
  // 만약 첫번째 obj에 동일한 key가 존재하면
  // 새로운 value로 덮어씌운다
  // 바뀐 첫번째 obj를 return 한다.
  /*
  if (_.every(objs, (obj) => Object.keys(obj).length === 0)) {
    return objs[0];
  }*/
 
  _.each(objs, (obj) => {
    for (let key in obj) {
      //반복문을 써서 obj1에 덮어씌우기
      //obj1의 reference는 유지된다.
      obj1[key] = obj[key];
    }
  });
  return obj1;
};
  • 기능: 여러 개의 객체를 입력받아, 순서대로 객체를 결합한다.
  • 첫 번째 입력인 객체를 기준으로 다음 순서의 객체들의 속성을 덮어쓴다.
  • 최종적으로 (속성이 추가된) 첫 번째 객체를 리턴한다. (새로운 객체 X)
  • _.extend로 전달되는 객체의 수는 정해져 있지 않습니다.
  • spread syntax 또는 arguments 객체를 사용해야 한다.
  • 함수의 시그니쳐(함수의 입력과 출력, 함수의 모양)를 적절하게 변형해도된다.
  • _.each를 사용해서 구현한다.
  • 예제 :
    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"

defaults 기능

_.defaults = function (obj1, ...objs) {
  // TODO: 여기에 코드를 작성한다.

  _.each(objs, (obj) => {
    for (let key in obj) {
      //반복문을 써서 obj1에 덮어씌우기
      //obj1에 key가 존재하면 아무것도 안한다.
      //key가 존재하지않으면 추가한다.
      if (!obj1.hasOwnProperty(key)) {
        obj1[key] = obj[key];
      }
    }
  });
  return obj1;
};
  • 기능: _.extend와 비슷하게 동작하지만, 이미 존재하는 속성(key)을 덮어쓰지 않는다.

zip 기능

_.zip = function (...arrs) {
  // TODO: 여기에 코드를 작성한다.

  // 여러개의 배열에서 동일한 index의 값으로 새로운 배열을 만든다.

  // arrs 반복문을 돌면서 각각의 요소값을 배열로 만들어준다.
  // 그 배열을 result라는 새 배열에 push 해준다.

  let result = [];
  let longest = 0;

  for (const arr of arrs) {
    if (arr.length > longest) {
      longest = arr.length;
    }
  }

  for (let i = 0; i < longest; i++) {
    result.push(_.pluck(arrs, i));
  }

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

zipStrict 기능

_.zipStrict = function (...arrs) {
  // TODO: 여기에 코드를 작성한다.
  let result = [];
  let shortest = 99999;

  for (const arr of arrs) {
    if (arr.length < shortest) {
      shortest = arr.length;
    }
  }

  for (let i = 0; i < shortest; i++) {
    result.push(_.pluck(arrs, i));
  }

  return result;
};
  • 기능 : _.zip과 비슷하게 동작하지만,최종적으로 리턴되는 배열의 각 요소의 길이는 입력으로 전달되는 배열 중 가장 '짧은' 배열의 길이로 통일된다.
  • 그 외 조건은 앞서 _.zip과 동일한다.

intersection 기능

_.intersection = function (set1, ...sets) {
  // TODO: 여기에 코드를 작성한다.

  // 반복문을 돌면서 set1의 요소가 set2에 있는지 확인
  // set2에 있으면 새로운 배열에 추가
  // 없으면 아무것도 안한다.

  let result = [];

  _.each(set1, (item) => {
    //set1의 각 요소가 sets에 있는 모든 배열에 있는지 확인
    if (
      /* every 함수로 sets의 모든 요소에 item이 있는지 확인 */
      _.every(sets, (set) => {
        //indexOf 함수를 사용해서 item이 존재하는지 확인
        // item이 없으면 -1을 return 하기 때문에 0 보다 큰지 확인
        if (_.indexOf(set, item) >= 0) {
          return true;
        }
        return false;
      })
    ) {
      result.push(item);
    }
  });

  return result;
};
  • 기능: 여러 개의 배열을 입력받아, 교집합 배열을 리턴한다.
  • 교집합 배열은 모든 배열에 공통으로 등장하는 요소들만을 요소로 갖는 배열입니다.
  • 교집합 배열의 요소들은 첫 번째 입력인 배열을 기준으로 한다.
  • 교집합이 없는 경우 빈 배열을 리턴한다.
  • 예제 :
    const set1 = ['a', 'e', b', 'c'];
    const set2 = ['c', 'd', 'e'];
    const set3 = ['a', 'b', 'e'];
    const result = _.intersection(set1, set2);
    console.log(result) // --> ['e', 'c'] => 첫 번째 배열에 'e'가 먼저 등장

difference 기능

_.difference = function (set1, ...sets) {
  // TODO: 여기에 코드를 작성한다.
  let result = [];
  
  _.each(set1, (item) => {
    if (
      !_.some(sets, (set) => {
        if (_.indexOf(set, item) < 0) {
          return false;
        }
        return true;
      })
    ) {
      result.push(item);
    }
  });
  return result;
};
  • 기능 : 여러 개의 배열을 입력받아, 차집합 배열을 리턴한다.
  • 차집합 배열은 첫 번째 배열에서 차례대로 다음 배열들의 요소들을 제외한 배열입니다.
  • 차집합 배열의 요소들은 첫 번째 입력인 배열을 기준으로 한다.
  • 차집합이 없는 경우 빈 배열을 리턴한다.
  • 예제 :
      const set1 = ['a', 'b', 'c'];   [1, 2, 3, 4],
      const set2 = ['b', 'c', 'd'];   [2, 30, 40],
      const set3 = ['a']               [1, 11, 111]
      const result = _.difference(set1, set2);
      console.log(result) // --> [3, 4]

Repeat again
해당문제는 _.some 사용에 있어서 아직 납득되지 않는 부분이 있어서 다시 테스트 케이스를 넣어서 적용해 볼 필요가 있다.

sortBy 기능

_.sortBy = function (arr, transform, order) {
  // transform이 있으면 transform을 사용해서 sort를 한다.
  // 없으면 배열의 요소 값 자체에 대한 비교 연산자의 결과를 따른다.
  // order = undefined 이거나 1 이면 오름차순, -1 이면 내림차순

  // transform이 있는지 확인
  let result = [...arr];
  if (transform !== undefined) {
    if (order === undefined || order === 1) {
      result.sort((a, b) => {
        if (transform(a) > transform(b)) {
          return 1;
        }
        if (transform(a) < transform(b)) {
          return -1;
        }
        return 0;
      });
    } else {
      result.sort((a, b) => {
        if (transform(a) > transform(b)) {
          return -1;
        }
        if (transform(a) < transform(b)) {
          return 1;
        }
        return 0;
      });
    }
  } else {
    if (order === undefined || order === 1) {
      result.sort((a, b) => a - b);
    } else {
      result.sort((a, b) => b - a);
    }
  }

  return result;
};
  • 기능: 배열의 각 요소에 함수 transform을 적용하여 얻은 결과를 기준으로 정렬한다.
  • transform이 전달되지 않은 경우, 배열의 요소 값 자체에 대한 비교 연산자의 결과를 따른다.


느낀점

아직 spread syntax를 사용하는게 익숙하지 않아서 구현하면서 중간에 헷깔리는 경우도 있었다.
그리고 함수를 매개변수로 갖는 함수(고차함수)를 가져와서 기능을 사용하려고 할때 어떤 인자값을 넣어야하는지 로직의 흐름이 어떻게 흘러가는 지 그려지지 않아서 처음에는 조금 고생했지만 여러 함수를 구현해보면서 조금은익숙해지고 흐름이 그려지는 같다.

추가 스터디가 필요한 부분 및 알게 된 것

  • 깊은 복사와 얕은 복사 (배열과 객체)
  • 고차함수, 클로저함수, 콜백함수, 커링 함수의 차이점
  • bubble sort, insertion sort, quick sort, merge sort, radix sort
profile
Frontend Developer

0개의 댓글