Underbar 라이브러리
과거에는 배열 메소드가 브라우저에서 자체적으로 지원되지 않던 시기가 있었다. 지금은 개발자들이 보다 나은 방법으로 배열이나 객체를 다루기 위한 라이브러리, 즉 Underbar 라이브러리를 만들었다. Underbar의 모티브가 되는 라이브러리는 underscore.js, lodahs 등이 있다.
이 중에서 Underscore.js 라이브러리의 collection을 다루는 여러 함수를 실습해보려고 한다.
*사용 가능한 내장 메소드 : pop, push, shift, sort
_.identity
전달인자(argument)가 무엇이든, 그대로 리턴한다.
.identity = function (val) {
return val;
};
_.slice
배열의 start 인덱스부터 end 인덱스 이전까지의 요소를 얕은 복사(shallow copy)하여 새로운 배열을 리턴한다. _.slcie는 변수를 선언할 경우, 콤마(,)를 이용해 선언할 수 있다. 이 때, 콤마로 연결된 변수들은 모두 동일한 선언 키워드(let, const)가 적용된다.
_.slice = function (arr, start, end) {
let _start = start || 0,
// `start`가 undefined인 경우, slice는 0부터 동작
_end = end;
// 입력받은 인덱스가 음수일 경우, 마지막 인덱스부터 매칭
// ex) -1 => arr.length - 1, -2 => arr.length - 2)
// 입력받은 인덱스는 0 이상이어야 한다.
if (start < 0) _start = Math.max(0, arr.length + start);
if (end < 0) _end = Math.max(0, arr.length + end);
// `end`가 생략될 경우(undefined), slice는 마지막 인덱스까지 동작
// `end`가 배열의 범위를 벗어날 경우, slice는 마지막 인덱스까지 동작
if (_end === undefined || _end > arr.length) _end = arr.length;
let result = [];
// `start`가 배열의 범위를 벗어날 경우, 빈 배열을 리턴
for (let i = _start; i < _end; i++) {
result.push(arr[i]);
}
return result;
};
_.take
_.take = function (arr, n) {
let result = [];
// n이 배열의 길이를 벗어날 경우, 전체 배열을 shallow copy한 새로운 배열을 리턴
if ( n >= arr.length) {
return arr;
}
for (let i = 0; i < n; i++ ) {
if ( n === undefined || n < 0 ) { // n이 undefined이거나 음수인 경우, 빈 배열을 리턴
return result;
}
else {
result.push(arr[i]); // n이 배열의 길이 안에 있으면 arr[i]를 추가
}
}
return result;
};
_.drop
_.drop = function (arr, n) {
let result = [];
// n이 배열의 길이를 벗어날 경우, 빈 배열을 리턴
if ( n >= arr.length) {
return result;
}
for (let i = 0; i < arr.length; i++ ) {
if ( n === undefined || n < 0 ) { // n이 undefined이거나 음수인 경우, 빈 배열을 리턴
result.push(arr[i]);
}
else if ( i >= n ) {
result.push(arr[i]); // n이 배열의 길이 안에 있으면 arr[i]를 추가
}
}
return result;
};
_.last
_.last = function (arr, n) {
if ( n === undefined || n < 0 ) {
return _.slice(arr, -1); // n이 undefined이거나 음수인 경우, 배열의 마지막 요소만을 담은 배열을 리턴
}
else if ( n > arr.length ) {
return _.take(arr, n); // n이 배열의 길이를 벗어날 경우, 전체 배열을 shallow copy한 새로운 배열을 리턴
}
else if ( n === 0 ) {
return []; // n이 0일 경우 빈 배열을 리턴
}
else {
return _.slice(arr, -n);
}
};
_.each
.each는 collection의 각 데이터에 반복적인 작업을 수행한다. collection(배열 혹은 객체)과 함수 iteratee(반복되는 작업)를 인자로 전달받아 (iteratee는 함수의 인자로 전달되는 함수이므로 callback 함수) collection의 데이터(element 또는 property)를 순회하면서 iteratee에 각 데이터를 인자로 전달하여 실행한다. .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 key in collection) {
iteratee(collection[key], key, collection);
}
}
};
_.indexof
_.indexOf = function (arr, target) {
let result = -1;
_.each(arr, function (item, index) {
if (item === target && result === -1) {
result = index;
}
});
return result;
};
_.filter
_.filter = function (arr, test) {
let result = [];
_.each(arr, function(el) {
if( test(el) ) {
result.push(el) // test(el)를 통과하고 각 요소에 반복적용
}
});
return result;
};
_.reject
.reject는 .filter와 정반대로 test 함수를 통과하지 않는 모든 요소를 담은 새로운 배열을 리턴한다.
_.reject = function (arr, test) {
let result = [];
_.each(arr, function(el) {
if( !test(el) ) {
result.push(el) // test(el)를 통과하고 각 요소에 반복적용
}
});
return result;
};
_.uniq
_.uniq = function (arr) {
let result = [];
for (let i = 0; i < arr.length; i++) {
if (_.indexOf(result, arr[i]) === -1 ) { // _.indexof 가 존재하지 않으면 -1이기 때문에
result.push(arr[i]);
}
}
return result;
};
_.map
_.map = function (arr, iteratee) {
// _.each 함수와 비슷하게 동작하지만, 각 요소에 iteratee를 적용한 결과를 리턴한다.
let result = [];
_.each(arr, function(el) {
result.push(iteratee(el));
});
return result;
};
_.pluck
_.pluck은 객체 또는 배열을 요소로 갖는 배열과 각 요소에서 찾고자 하는 key 또는 index를 입력받아 각 요소의 해당 값 또는 요소만을 추출하여 새로운 배열에 저장하고, 최종적으로 새로운 배열을 리턴한다.
_.pluck = function (arr, keyOrIdx) {
// _.each를 사용해 구현
let result = [];
_.each(arr, function (item) {
result.push(item[keyOrIdx]);
});
return result;
}
_.pluck = function (arr, keyOrIdx) {
// _.map를 사용해 구현
let result = [];
_.map(arr, function (item) {
result.push(item[keyOrIdx]);
});
return result;
}
_.reduce
.reduce는 배열을 순회하며 각 요소에 iteratee 함수를 적용하고, 그 결과값을 계속해서 누적(accumulate)하고, 최종적으로 누적된 결과값을 리턴한다. .reduce는 배열이라는 다수의 정보가 하나의 값으로 축소(응축, 환원, reduction)되기 때문에 reduce라는 이름이 붙게 된 것이다. .reduce는 위에서 구현한 많은 함수처럼, 입력으로 배열과 각 요소에 반복할 작업(iteratee)을 전달받습니다. iteratee는 차례대로 데이터(element 또는 value), 접근자(index 또는 key), collection을 다룰 수 있어야 한다. 배열 arr을 입력받을 경우, iteratee(ele, idx, arr) .reduce는 반복해서 값을 누적하므로 이 누적되는 값을 관리해야 한다.
여기까지 정리하면 다음과 같다.
_.reduce(arr, iteratee)
iteratee(acc, ele, idx, arr)
여기서 중요한것은, '누적값은 어디서부터 시작하는가'라는 것인데, 이를 해결하는 방법은 초기 값을 직접 설정하거나 자동으로 설정하는 것이다. _.reduce는 세 번째 인자로 초기 값을 전달받을 수 있기 때문에,
따라서 최종적인 형태는
_.reduce(arr, iteratee, initVal)
iteratee(acc, ele, idx, arr)
_.reduce = function (arr, iteratee, initVal) {
let result = initVal;
_.each(arr, function(el, idx) {
if ( result === undefined ) {
result = el; // 초기 값이 주어지지 않았다면, 배열의 첫 요소가 result가 된다.
}
else {
result = iteratee(result, el, idx, arr);
}
});
return result;
};