[SEB_FE_45] 2023.05.15 / 배열, 객체 메서드 JS로 구현하기

Kay·2023년 5월 15일
0

오늘의 과제는 underscore.js 라이브러리의 메서드 또는 자바스크립트의 배열, 객체 내장 메서드 중 일부를 구현하는 것이다.

underscore.js는 배열 메서드가 브라우저에서 자체적으로 지원되지 않던 시절 만들어진 라이브러리라고 한다.

Minimum 과제

identity

전달인자(argument)가 무엇이든, 그대로 리턴

_.identity = function (val) {
  return val;
};

slice

배열의 start 인덱스부터 end 인덱스 이전까지의 요소를 shallow copy하여 새로운 배열을 리턴

  • start가 배열의 범위를 벗어날 경우, 빈 배열을 리턴
  • start 또는 end가 음수일 경우, 마지막 인덱스부터 매칭
  • end가 생략될 경우(undefined), end가 배열의 범위를 벗어날 경우, slice는 마지막 인덱스까지 동작
_.slice = function (arr, start, end) {
  let _start = start || 0, _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;
};

take

배열의 처음 n개의 element를 담은 새로운 배열을 리턴

  • n이 undefined이거나 음수인 경우, 빈 배열을 리턴
  • n이 배열의 길이를 벗어날 경우, 전체 배열을 shallow copy한 새로운 배열을 리턴
_.take = function (arr, n) {
  if (n === undefined || n < 0) {
    return [];
  }

  return _.slice(arr, 0, n);
};

drop

처음 n개의 element를 제외한 새로운 배열을 리턴

  • n이 undefined이거나 음수인 경우, 전체 배열을 shallow copy한 새로운 배열을 리턴
  • n이 배열의 길이를 벗어날 경우, 빈 배열을 리턴
_.drop = function (arr, n) {
  if (n === undefined || n < 0) {
    return [...arr];
  }

  return _.slice(arr, n);
};

last

배열의 마지막 n개의 element를 담은 새로운 배열을 리턴

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

  if (arr.length < n) {
    return [...arr];
  }

  return _.slice(arr, arr.length - n);
};

each

_.each는 collection의 각 데이터에 반복적인 작업을 수행

  • 배열 arr을 입력받을 경우, iteratee(ele, idx, arr)
  • 객체 obj를 입력받을 경우, iteratee(val, key, obj)
_.each = function (collection, iteratee) {
  if (typeof collection !== 'object') {
    console.error("first parameter is not a object");
    return;
  }

  // 배열 arr을 입력받을 경우, iteratee(ele, idx, arr)
  // 1. 배열을 입력받은 경우, 배열을 순회하면서 배열의 요소에 접근할 수 있어야 합니다.
  // 2. 배열을 입력받은 경우, 배열을 순회하면서 배열의 요소와 인덱스에 접근할 수 있어야 합니다.
  // 3. 배열을 입력받은 경우, 배열을 순회하면서 배열의 요소, 인덱스, 배열 그 자체에 접근할 수 있어야 합니다.
  if (Array.isArray(collection)) {
    for (let i = 0; i < collection.length; i++) {
      iteratee(collection[i], i, collection);
    }

    return;
  }

  // 객체 obj를 입력받을 경우, iteratee(val, key, obj)
  // 1. 객체를 입력받은 경우, 객체의 속성값에 접근할 수 있어야 합니다.
  // 2. 객체를 입력받은 경우, 객체의 속성값, 속성이름에 접근할 수 있어야 합니다.
  // 3. 객체를 입력받은 경우, 객체의 속성값, 속성이름, 객체 자체에 접근할 수 있어야 합니다.
  for (let key in collection) {
    iteratee(collection[key], key, collection);
  }

  return;
};

indexOf

target으로 전달되는 값이 arr의 요소인 경우, 배열에서의 위치(index)를 리턴

  • 그렇지 않은 경우, -1을 리턴
  • target이 중복해서 존재하는 경우, 가장 낮은 index를 리턴
_.indexOf = function (arr, target) {
  let result = -1;

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

  return result;
};

filter

test 함수를 통과하는 모든 요소를 담은 새로운 배열을 리턴

_.filter = function (arr, test) {
  let result = [];

  _.each(arr, (ele) => {
    if (test(ele)) {
      result.push(ele);
    }
  })

  return result;
};

reject

test 함수를 통과하지 않는 모든 요소를 담은 새로운 배열을 리턴

  let result = [];

  _.each(arr, (ele) => {
    if (!test(ele)) {
      result.push(ele);
    }
  })

  return result;

uniq

주어진 배열의 요소가 중복되지 않도록 새로운 배열을 리턴

_.uniq = function (arr) {
  let result = [];

  _.each(arr, (ele) => {
    if (_.indexOf(result, ele) < 0) {
      result.push(ele);
    }
  })

  return result;
};

map

_.each 함수와 비슷하게 동작하지만, 각 요소에 iteratee를 적용한 결과를 리턴

_.map = function (arr, iteratee) {
  let result = [];

  _.each(arr, (ele, idx) => {
    const value = iteratee(ele, idx);
    result.push(value);
  })

  return result;
};

pluck

_.pluck = function (arr, keyOrIdx) {
  return _.map(arr, (ele) => ele[keyOrIdx]);
};

reduce

  1. 객체 또는 배열을 요소로 갖는 배열과 각 요소에서 찾고자 하는 key 또는 index를 입력받아
  2. 각 요소의 해당 값 또는 요소만을 추출하여 새로운 배열에 저장하고,
  3. 최종적으로 새로운 배열을 리턴
_.reduce = function (arr, iteratee, initVal) {
  const isInitValExist = initVal !== undefined;
  let _arr = [...arr];

  if (isInitValExist) {
    _arr = [initVal, ..._arr];
  }
  
  if (_arr.length < 2) {
    return _arr[0];
  }

  let acc = _arr[0];
  _.each(arr, (ele, idx) => {
    if (!isInitValExist && idx === 0) {
      return;
    }

    acc = iteratee(acc, ele, idx, arr);
  })

  return acc;
};

Advanced 과제

once

callback 함수를 한 번만 호출하는 '함수'를 리턴

_.once = function (func) {
  let result = [];

  return function (...arg) {
    // TIP: arguments 키워드 혹은, spread operator를 사용하세요.
    if (result.length === 1) {
      return result[0];
    }

    result.push(func(...arg));
    return result[0];
  };
};

delay

_.delay(func, 500, 'a', 'b')의 결과로 '최소' 500m가 지난 이후에 func('a', 'b')가 호출

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

includes

배열이 주어진 값을 포함하는지 확인

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

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

  return result;
};

every

배열의 모든 요소가 test 함수(iteratee)를 통과하면 true를, 그렇지 않은 경우 false를 리턴

  • 빈 배열을 입력받은 경우, true를 리턴
_.every = function (arr, iteratee) {
  let result = true;
  if (arr.length === 0) {
    return result;
  }

  _.each(arr, (ele) => {
    if (iteratee) {
      if (!iteratee(ele)) {
        result = false;
      }
    }

    if (!iteratee) {
      if (!ele) {
        result = false;
      }
    }
  })

  return result;
};

some

배열의 요소 중 하나라도 test 함수(iteratee)를 통과하면 true를, 그렇지 않은 경우 false를 리턴

_.some = function (arr, iteratee) {
  let result = false;
  if (arr.length === 0) {
    return result;
  }

  _.each(arr, (ele) => {
    if (iteratee) {
      if (iteratee(ele)) {
        result = true;
      }
    }

    if (!iteratee) {
      if (ele) {
        result = true;
      }
    }
  })

  return result;
};

extend

여러 개의 객체를 입력받아, 순서대로 객체를 결합
첫 번째 입력인 객체를 기준으로 다음 순서의 객체들의 속성을 덮어쓰기에 새로운 객체를 반환하는 것이 아님

_.extend = function (obj, ...args) {
  // _.each(args, (ele) => {
  //   // 객체 합치는 방법
  //   // 1. Object.assign()
  //   // - 원본 개체에서 대상 개체로 속성을 복사 -> 즉, 새로운 객체가 아님
  //   // - 동일한 키가 발견되면 소스의 속성이 대상 개체의 속성을 덮어씀
  //   Object.assign(obj, ele);
  //   // 2. spread 연산자
  //   // - 두 개채의 속성을 새 개체로 병합 -> 즉, 새로운 객체 생성
  //   // - 동일한 키가 발견되면 소스의 속성이 대상 개체의 속성을 덮어 쓰지 않음
  //   // obj = {...obj, ...ele};
  // })

  _.each(args, (ele) => {
    _.each(ele, (val, key) => {
      obj[key] = val;
    })
  })

  return obj;
};

defaults

_.extend와 비슷하게 동작하지만, 이미 존재하는 속성(key)을 덮어쓰지 않음

_.defaults = function (obj, ...args) {
  _.each(args, (ele) => {
    _.each(ele, (val, key) => {
      if (obj[key] === undefined) {
        obj[key] = val;
      }
    })
  })

  return obj;
};

zip

여러 개의 배열을 입력받아, 같은 index의 요소들을 묶어 만든 배열 반환

  • 최종적으로 리턴되는 배열의 각 요소의 길이는 입력으로 전달되는 배열 중 가장 '긴' 배열의 길이로 통일
  • 특정 index에 요소가 없는 경우, undefined를 사용

예시

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) {
  const LENGTH = _.reduce(args, (acc, cur) => {
    return acc < cur.length ? cur.length : acc;
  }, 0)

  let arr = [];

  for (let i = 0; i < LENGTH; i++) {
    arr.push(_.pluck(args, i));
  }

  return arr;
};

zipStrict

_.zip과 비슷하게 동작하지만, 최종적으로 리턴되는 배열의 각 요소의 길이는 입력으로 전달되는 배열 중 가장 '짧은' 배열의 길이로 통일

_.zipStrict = function (...args) {
  const LENGTH = _.reduce(args, (acc, cur) => {
    return acc > cur.length ? cur.length : acc;
  }, args[0].length)

  let arr = [];

  for (let i = 0; i < LENGTH; i++) {
    arr.push(_.pluck(args, i));
  }

  return arr;
};

intersection

여러 개의 배열을 입력받아, 교집합 배열을 리턴

  • 교집합 배열의 요소들은 첫 번째 입력인 배열을 기준
_.intersection = function (criteria, ...rest) {
  let result = [];
  _.each(criteria, (ele) => {
    const intersected = _.every(rest, (arr) => {
      return _.includes(arr, ele);
    });

    if (intersected) {
      result.push(ele);
    }
  });

  return result;
};

difference

여러 개의 배열을 입력받아, 차집합 배열을 리턴

_.difference = function (criteria, ...rest) {
  let result = [];
  _.each(criteria, (ele) => {
    const intersected = _.every(rest, (arr) => {
      return !_.includes(arr, ele);
    });

    if (intersected) {
      result.push(ele);
    }
  });

  return result;
};

sortBy

예시

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 }]

코드

_.sortBy = function (arr, transform, order) {
  const _arr = _.filter(arr, (ele) => ele);
  const _order = order || 1;
  const _transform = transform || _.identity;

  const compareFn = (a, b) => {
    const A = _transform(a);
    const B = _transform(b);

    if (A > B) return _order;
    if (A < B) return -1 * _order;
    return 0;
  }

  return _arr.sort(compareFn);
};

nightmare 과제

memoize

메모이제이션은 이미 해결한 문제는 다시 풀지 않는 기법

  • 같은 상태에서 출발한 함수는 항상 같은 결과를 리턴
  • 문제를 해결할 때 마다 해당 문제의 답을 기록(메모)해두고, 다음에 동일한 문제를 풀 상황이 오면, 앞서 기록한 답을 활용

출처: medium - JavaScript Generic Memoization 구현하기

_.memoize = function (func) {
  const cache = {};
  return (...args) => {
    if (cache[args]) {
      console.log(`${args} already called, return from cache`);
      return cache[args];
    }

    console.log(`${args} never called, execute and cache`);
    cache[args] = func(...args);

    return cache[args];
  }
};

throttle

입력으로 전달되는 시간(ms, 밀리초)동안에 callback 함수를 단 한번만 실행되는 함수를 반환

출처: velog - Throttle, Debounce 개념과 구현

_.throttle = function (func, wait) {
  let isExecuting = false;
  return (...args) => {
    if (!isExecuting) {
      isExecuting = true;
      func(...args);

      setTimeout(() => {
        isExecuting = false;
      }, wait);
    }
  }
};

0개의 댓글

관련 채용 정보