_.identity = function (val) {
return val;
};
전달인자(argument)가 무엇이든, 그대로 리턴하며 underbar의 기능 구현 및 테스트를 위해 재사용되는 함수이다.
collection은 영어로 '모음, 무리'라는 뜻이다.
컴퓨터 과학에서는 '데이터(data, 자료)의 모음'으로 부를 수 있다.
- collection의 종류는 다양하게 있지만 배열과 객체가 대표적이다.
- 배열 : 데이터들(요소, element)을 '순서대로' 모은 자료 구조로 요소의 위치를 통해 데이터에 접근할 수 있다.
- 객체 : 서로 관련있는 데이터들(속성, property)을 'key-value' 형태로 '순서 없이' 모은 자료 구조로 속성에 부여된 키(key)를 통해 데이터에 접근할 수 있다.
- collection의 각 데이터를 다루는 것은 매우 흔한 작업 중에 하나이다.
_.slice = function (arr, start, end) {
// 변수를 선언할 경우, 아래와 같이 콤마(,)를 이용해 선언할 수 있다.
// 이때, 콤마로 연결된 변수들은 모두 동일한 선언 키워드(let, const)가 적용된다.
let _start = start || 0, // `start`가 undefined인 경우, slice는 0부터 동작한다.
_end = end; // 입력받은 인덱스가 음수일 경우, 마지막 인덱스부터 매칭한다.
(예. -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 = function (arr, n) {
let result = [];
if(n >= arr.length) {
return arr;
} // n이 배열의 길이를 벗어날 경우, 전체 배열을 shallow copy한 새로운 배열을 리턴한다.
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;
};
배열의 처음 n개의 element를 담은 새로운 배열을 리턴한다.
_.drop = function (arr, n) {
let result = [];
if(n >= arr.length) {
return result;
} // n이 배열의 길이를 벗어날 경우, 빈 배열을 리턴한다.
for(let i = 0; i < arr.length; i++) {
if(n === undefined || n < 0) { // n이 undefined이거나 음수인 경우, 전체 배열을 shallow copy한 새로운 배열을 리턴한다.
result.push(arr[i]);
} else if(i >= n){
result.push(arr[i]); // n이 배열의 길이 안에 있으면 arr[i]를 빈 배열에 추가한다.
}
}
return result;
};
처음 n개의 element를 제외한 새로운 배열을 리턴한다.
_.last = function (arr, n) {
let result = [];
if(n === undefined || n < 0) { // n이 undefined이거나 음수인 경우, 배열의 마지막 요소만을 담은 배열을 리턴한다.
result.push(arr[arr.length-1]);
return result;
}
for(let i = 0; i < arr.length; i++) {
if(n >= arr.length) { // n이 배열의 길이를 벗어날 경우, 전체 배열을 shallow copy한 새로운 배열을 리턴힌다.
return arr;
} else { // n의 범위인 경우 배열의 길이에서 n을 뺀 위치의 길이에서 배열의 길이까지 리턴한다.
return _.slice(arr,arr.length -n,arr.length);
}
}
return result;
};
_.last = function(arr, n) {
if(n === undefined) {
return array[arr.length-1];
} else if(arr.length <= n) {
return arr;
} else{
return arr.slice(arr.length-n, arr.length);
}
};
배열의 마지막 n개의 element를 담은 새로운 배열을 리턴한다.
_.each = function (collection, iteratee) {
if(Array.isArray(collection)) { // collection이 배열이면
for(let i = 0; i < collection.length; i++) {
iteratee(collection[i], i, collection);
} // collection[i]: element , i: index , collenction: array
} else if(typeof collection === 'object'){ // collection의 타입이 객체면
for(let key in collection) {
iteratee(collection[key], key ,collection);
} // collection[key]: value , key: key , collection : object
}
};
collection의 각 데이터에 반복적인 작업을 수행한다.
- collection(배열 혹은 객체)과 함수 iteratee(반복되는 작업)를 인자로 전달받아 (iteratee는 함수의 인자로 전달되는 함수이므로 callback 함수)
- collection의 데이터(element 또는 property)를 순회하면서
- iteratee에 각 데이터를 인자로 전달하여 실행한다.
iteratee는 차례대로 데이터(element 또는 value), 접근자(index 또는 key), collection을 다룰 수 있어야 한다.
- 배열 arr을 입력받을 경우, iteratee(ele, idx, arr)
- 객체 obj를 입력받을 경우, iteratee(val, key, obj)
- 이처럼 collection의 모든 정보가 iteratee의 인자로 잘 전달되어야 모든 경우를 다룰 수 있다.
- 실제로 전달되는 callback 함수는 collection의 모든 정보가 필요하지 않을 수도 있다.
_.indexOf = function (arr, target) {
let result = -1;
_.each(arr, function (item, index) {
if (item === target && result === -1) { // result가 -1이고,
요소와 target이 일치하면 result에 해당 index를 할당한다.
result = index;
}
});
return result;
};
// 여기서 `result === -1`을 조건에 넣어준 이유는 target과 중복인 item이 여러개일때
첫 중복 item의 index를 result에 할당해주면,
이후 중복인 item이 조건문에 들어갔을 때 result === -1이 성립되지 않기 때문에
result 값은 첫 중복 item의 index 그대로 리턴되기 때문이다.
for(let i=0; i < array.length; i++){
if(array[i] === target){
return i;
}
}
return -1;
target으로 전달되는 값이 배열에서 발견되면, 그 index를 반환하고, 만일 배열에서 발견할 수 없다면 -1을 반환한다.
target이 중복해서 존재하는 경우, 가장 낮은 index를 리턴한다.
_.filter = function (arr, test) {
let result = [];
_.each(arr, function(el) {
if(test(el)) { // test(element)의 결과(return 값)가 truthy일 경우 통과하며 test 함수는 각 요소에 반복 적용된다.
result.push(el);
}
});
return result;
};
_.filter = function(arr, test) {
let result = [];
for(let i=0; i < arr.length; i++){
if(test(arr[i])){
result.push(arr[i]);
}
}
return result;
};
test 함수를 통과하는 모든 요소를 담은 새로운 배열을 리턴한다.
_.reject = function (arr, test) {
let result = [];
_.each(arr, function(el) {
if(!test(el)) { // test(element)의 결과(return 값)가 falsy일 경우 통과하며 test 함수는 각 요소에 반복 적용된다.
result.push(el);
}
});
return result;
};
_.reject = function(arr, test) {
let result = [];
for(let i=0; i < arr.length; i++){
if(!test(arr[i])){
result.push(carr[i]);
}
}
return result;
};
_.reject = function(arr, test) {
return _.filter(arr, function(el){
return !test(el);
});
};
test 함수를 통과하지 않는 모든 요소를 담은 새로운 배열을 리턴한다. (_.filter와 반대)
_.uniq = function (arr) {
let result = [];
_.each(arr, function(element) {
for (let i = 0; i < arr.length; i++) {
if (result[i] === element) {
return false; // for문안에서 result[i]가 element와 같을때 false로 리턴해서 for문 밖으로 넘어가는것을 막아준다.
}
}
result.push(element); // each와 for문안에서 중복되지 않은 요소들 push.
});
return result;
};
_.uniq = function(arr) {
let result = [];
for(let i=0; i < arr.length; i++){
if(_.indexOf(result, arr[i]) === -1){
result.push(arr[i]);
}
}
return result;
};
_.uniq = function(arr) {
let result = [];
_.each(arr, function(el){
if(_.indexOf(arr, el) === -1){
result.push(el);
}
});
return result;
};
_.uniq = function(arr) {
let obj = {};
for(let i=0; i < arr.length; i++){
obj[arr[i]] = arr[i];
}
return Object.values(obj);
};
주어진 배열의 요소가 중복되지 않도록 새로운 배열을 리턴한다.
_.map = function (arr, iteratee) {
let result = [];
_.each(arr,function(el){
result.push(iteratee(el))
});
return result;
};
_.map = function(arr, iteratee) {
let result = [];
for(let i=0; i < arr.length; i++){
result.push(iteratee(arr[i]));
}
return result;
};
iteratee(반복되는 작업)를 배열의 각 요소에 적용(apply)한 결과를 담은 새로운 배열을 리턴한다.
배열의 각 요소를 다른 것(iteratee의 결과)으로 매핑(mapping)한다.
_.pluck = function (arr, keyOrIdx) {
let result = [];
_.map(arr, function (item) {
result.push(item[keyOrIdx]);
});
return result;
};
_.pluck = function(arr, keyOrIdx) {
return _.map(arr, function(item) {
return item[keyOrIdx];
});
};
collection의 key값이 동일한 값들만을 모아 반환한다.
- 객체 또는 배열을 요소로 갖는 배열과 각 요소에서 찾고자 하는 key 또는 index를 입력받아
- 각 요소의 해당 값 또는 요소만을 추출하여 새로운 배열에 저장하고,
- 최종적으로 새로운 배열을 리턴한다.
ex)
각 개인의 정보를 담은 객체를 요소로 갖는 배열을 통해서, 모든 개인의 나이만으로 구성된 별도의 배열을 만들 수 있다.
최종적으로 리턴되는 새로운 배열의 길이는 입력으로 전달되는 배열의 길이와 같아야 한다.
따라서 찾고자 하는 key 또는 index를 가지고 있지 않은 요소의 경우, 추출 결과는 undefined 이다.
_.reduce = function (arr, iteratee, initVal) {
let acc = initVal;
_.each (arr, function(item, idx) {
if(initVal === undefined && idx === 0) { // 만약 initVal이 없고 인덱스가 0번째이면 초기값을 한번만 처음에 넣어준다
acc = item;
}
else {
acc = iteratee(acc, item, idx, arr);
}
});
return acc;
};
_.reduce = function (arr, iteratee, initVal) {
let index = 0;
if(initVal === undefined){
initVal = arr[0];
index = 1;
}
for(let i=index; i < arr.length; i++){
initVal = iteratee(initVal, arr[i])
}
return initVal;
};
_.reduce = function(arr, iteratee, initVal) {
let slice = Array.prototype.slice;
if(initVal === undefined){
initVal = arr[0];
arr = slice.call(arr,1);
}
_.each(arr, function(el){
initVal = iteratee(initVal, el);
});
return initVal
};
입력받은 collection으로부터 iteratee의 내용이 무엇인지에 따라서 하나의 결과값을 반환한다.
accumulator는 초기값으로, 이 accumulator가 주어지느냐, 주어지지않느냐의 두가지 경우로 나뉜다.
- 배열을 순회하며 각 요소에 iteratee 함수를 적용하고,
- 그 결과값을 계속해서 누적(accumulate)한다.
- 최종적으로 누적된 결과값을 리턴한다.
_.reduce는 위에서 구현한 많은 함수처럼, 입력으로 배열과 각 요소에 반복할 작업(iteratee)을 전달받습니다.
iteratee에 대해서 복습하면 아래와 같습니다. (일반적으로 객체를 reduce 하지는 않으므로, 배열 부분만 복습합니다.)
iteratee는 차례대로 데이터(element 또는 value), 접근자(index 또는 key), collection을 다룰 수 있어야 합니다.
배열 arr을 입력받을 경우,iteratee(ele, idx, arr)
.reduce는 반복해서 값을 누적하므로 이 누적되는 값을 관리해야 한다.
따라서 .reduce의 iteratee는 인자가 하나 더 추가되어 최종 형태는 아래와 같다.iteratee(acc, ele, idx, arr)
누적되는 값은 보통 tally, accumulator(앞글자만 따서 acc로 표기하기도 함)로 표현하거나
목적을 더 분명하게 하기 위해 sum(합), prod(곱), total 등으로 표현하기도 한다.
이때, acc는 '이전 요소까지'의 반복 작업의 결과로 누적된 값이다.
ele는 잘 아시다시피 반복 작업을 수행할(아직 수행하지 않은) 현재의 요소이다.
여기까지 내용을 정리하면 다음과 같다.
_.reduce(arr, iteratee) iteratee(acc, ele, idx, arr)
그런데 사실 누적값에 대해서 빠뜨린 게 하나 있다.
바로 '누적값은 어디서부터 시작하는가'라는 의문에 대한 대답을 하지 않았다.
이를 해결하는 방법은 초기 값을 직접 설정하거나 자동으로 설정하는 것이다.
_.reduce는 세 번째 인자로 초기 값을 전달받을 수 있다.
이 세 번째 인자로 초기 값이 전달되는 경우, 그 값을 누적값의 기초(acc)로 하여 배열의 '첫 번째' 요소부터 반복 작업이 수행된다.
반면 초기 값이 전달되지 않은 경우, 배열의 첫 번째 요소를 누적값의 출발로 하여 배열의 '두 번째' 요소부터 반복 작업이 수행된다.
따라서 최종적인 형태는 아래와 같다.
_.reduce(arr, iteratee, initVal) iteratee(acc, ele, idx, arr)