오늘의 과제는 underscore.js 라이브러리의 메서드 또는 자바스크립트의 배열, 객체 내장 메서드 중 일부를 구현하는 것이다.
underscore.js는 배열 메서드가 브라우저에서 자체적으로 지원되지 않던 시절 만들어진 라이브러리라고 한다.
전달인자(argument)가 무엇이든, 그대로 리턴
_.identity = function (val) {
return val;
};
배열의 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;
};
배열의 처음 n개의 element를 담은 새로운 배열을 리턴
_.take = function (arr, n) {
if (n === undefined || n < 0) {
return [];
}
return _.slice(arr, 0, n);
};
처음 n개의 element를 제외한 새로운 배열을 리턴
_.drop = function (arr, n) {
if (n === undefined || n < 0) {
return [...arr];
}
return _.slice(arr, n);
};
배열의 마지막 n개의 element를 담은 새로운 배열을 리턴
_.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는 collection의 각 데이터에 반복적인 작업을 수행
_.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;
};
target으로 전달되는 값이 arr의 요소인 경우, 배열에서의 위치(index)를 리턴
_.indexOf = function (arr, target) {
let result = -1;
_.each(arr, function (item, index) {
if (item === target && result === -1) {
result = index;
}
});
return result;
};
test 함수를 통과하는 모든 요소를 담은 새로운 배열을 리턴
_.filter = function (arr, test) {
let result = [];
_.each(arr, (ele) => {
if (test(ele)) {
result.push(ele);
}
})
return result;
};
test 함수를 통과하지 않는 모든 요소를 담은 새로운 배열을 리턴
let result = [];
_.each(arr, (ele) => {
if (!test(ele)) {
result.push(ele);
}
})
return result;
주어진 배열의 요소가 중복되지 않도록 새로운 배열을 리턴
_.uniq = function (arr) {
let result = [];
_.each(arr, (ele) => {
if (_.indexOf(result, ele) < 0) {
result.push(ele);
}
})
return result;
};
_.each 함수와 비슷하게 동작하지만, 각 요소에 iteratee를 적용한 결과를 리턴
_.map = function (arr, iteratee) {
let result = [];
_.each(arr, (ele, idx) => {
const value = iteratee(ele, idx);
result.push(value);
})
return result;
};
_.pluck = function (arr, keyOrIdx) {
return _.map(arr, (ele) => ele[keyOrIdx]);
};
_.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;
};
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(func, 500, 'a', 'b')
의 결과로 '최소' 500m가 지난 이후에 func('a', 'b')
가 호출
_.delay = function (func, wait, ...args) {
setTimeout(() => func(...args), wait);
};
배열이 주어진 값을 포함하는지 확인
_.includes = function (arr, target) {
let result = false;
_.each(arr, (ele) => {
if (ele === target) {
result = true;
}
})
return result;
};
배열의 모든 요소가 test 함수(iteratee)를 통과하면 true를, 그렇지 않은 경우 false를 리턴
_.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;
};
배열의 요소 중 하나라도 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 = 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;
};
_.extend와 비슷하게 동작하지만, 이미 존재하는 속성(key)을 덮어쓰지 않음
_.defaults = function (obj, ...args) {
_.each(args, (ele) => {
_.each(ele, (val, key) => {
if (obj[key] === undefined) {
obj[key] = val;
}
})
})
return obj;
};
여러 개의 배열을 입력받아, 같은 index의 요소들을 묶어 만든 배열 반환
예시
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;
};
_.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 = 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 = function (criteria, ...rest) {
let result = [];
_.each(criteria, (ele) => {
const intersected = _.every(rest, (arr) => {
return !_.includes(arr, ele);
});
if (intersected) {
result.push(ele);
}
});
return result;
};
예시
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);
};
메모이제이션은 이미 해결한 문제는 다시 풀지 않는 기법
출처: 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];
}
};
입력으로 전달되는 시간(ms, 밀리초)동안에 callback 함수를 단 한번만 실행되는 함수를 반환
출처: velog - Throttle, Debounce 개념과 구현
_.throttle = function (func, wait) {
let isExecuting = false;
return (...args) => {
if (!isExecuting) {
isExecuting = true;
func(...args);
setTimeout(() => {
isExecuting = false;
}, wait);
}
}
};