p r o g r a m m i n g

underbar 과제 리뷰

hee.hee 2022. 5. 28. 01:15

 

underbar 과제가 어려웠던 이유는...

아마도 콜백 함수와 메서드들에 대한 이해가 제대로 되어 있지 않아서.

작성했던 코드를 보면서 한줄씩 해설해보자,


 

 

_.each

첫번째로, 문제의 each이다. foreach처럼 배열, 혹은 객체의 요소 / 속성들을 순회하며 각 요소 / 속성 값에 접근할 수 있다.

동시에 익명함수(뭐가 올지 모른다) iteratee에 모든 요소 / 속성을 인자로 전달한다.

그러니까, each는 객체 혹은 배열을 돌고, 각 배열 혹은 객체의 요소 / 속성에 함수 iteratee를 적용시킨다. 

리턴 값은 없다. each는 적용시킨 값을 갖고만 있고, 앞으로 만들 함수 안에서 필요한 값을 리턴 시켜주면 된다.

더보기
// _.each는 collection의 각 데이터에 반복적인 작업을 수행합니다.
// 1. collection(배열 혹은 객체)과 함수 iteratee(반복되는 작업)를 인자로 전달받아 (iteratee는 함수의 인자로 전달되는 함수이므로 callback 함수)
// 2. collection의 데이터(element 또는 property)를 순회하면서
// 3. iteratee에 각 데이터를 인자로 전달하여 실행합니다.
// iteratee에는 테스트 케이스에 따라서 다양한 함수가 할당됩니다.
// Array.prototype.forEach 메소드를 사용할 때, 다양한 형태의 callback 함수를 사용할 수 있었던 걸 기억하시나요?
// 우리가 만드는 _.each 함수도 그렇게 잘 작동하게 하기 위한 방법을 고민해 봅시다.
/*
* SpecRunner를 열고 each의 네 번째 테스트 케이스를 눌러 보시기 바랍니다.
* 이 테스트 케이스의 collection은 letters이고,
* iteratee는 익명함수 function(letter) { iterations.push(letter); }); 입니다.
*
* const letters = ['a', 'b', 'c'];
* const iterations = _endtter);
* });
* expect(iterations).to.eql(['a', 'b', 'c']);
*
* iteratee는 차례대로 데이터(element 또는 value), 접근자(index 또는 key), collection을 다룰 수 있어야 합니다.
* 배열 arr을 입력받을 경우, iteratee(ele, idx, arr)
* 객체 obj를 입력받을 경우, iteratee(val, key, obj)
* 이처럼 collection의 모든 정보가 iteratee의 인자로 잘 전달되어야 모든 경우를 다룰 수 있습니다.
* 실제로 전달되는 callback 함수는 collection의 모든 정보가 필요하지 않을 수도 있습니다.
*/
// _.each는 명시적으로 어떤 값을 리턴하지 않습니다.
_.each = function (collection, iteratee) { // collection은 배열이나 객체, iteratee는 콜백함수
if(Array.isArray(collection)){ //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는 target으로 전달되는 값이 arr의 요소인 경우, 배열에서의 위치(index)를 리턴합니다.
// 그렇지 않은 경우, -1을 리턴합니다.
// target이 중복해서 존재하는 경우, 가장 낮은 index를 리턴합니다.
// 배열의 모든 요소에 접근하려면, 순회 알고리즘(iteration algorithm)을 구현해야 합니다.
// 반복문을 사용하는 것이 가장 일반적이지만, 지금부터는 이미 구현한 _.each 함수를 활용하여야 합니다.
// 아래 _.indexOf의 구현을 참고하시기 바랍니다.
_.indexOf = function (arr, target) {

let result = -1;
_.each(arr, function (item, index) {  //arr의 요소를 순회하며 / 익명함수를 실행한다
if (item === target && result === -1) { //item(요소)이 찾고자하는 target과 일치하고 result값이 초기 값과 같다면
result = index; //result 값은 해당 요소의 인덱스 넘버로 바뀐다
}
});
return result; //target과 일치하는 item의 인덱스 넘버 or -1을 리턴한다
};

 

 

 

 

 

_.filter

더보기
// _.filter는 test 함수를 통과하는 모든 요소를 담은 새로운 배열을 리턴합니다.
// test(element)의 결과(return 값)가 truthy일 경우, 통과입니다.
// test 함수는 각 요소에 반복 적용됩니다.
_.filter = function (arr, test) {
// TODO: 여기에 코드를 작성합니다.
let result = [];
_.each(arr, function (item) { //filter 함수에서는 arr의 item이 필요함
if (test(item)) {  //item을 주어진 test(filter함수의 파라미터)의 인자로 전달한다. test가 실행됨. 그 값이 true라면
result.push(item); // item을 결과 값에 넣는다.
}
})
return result; //결과 리턴
};

 

 

 

 

 

 

_.reject

더보기
// _.reject는 _.filter와 정반대로 test 함수를 통과하지 않는 모든 요소를 담은 새로운 배열을 리턴합니다.
// TIP: 위에서 구현한 `filter` 함수를 사용해서 `reject` 함수를 구현해 보세요.
_.reject = function (arr, test) { 
let result = [];
_.each(arr, function (item) { //콜백 함수가 arr의 요소들에 접근해 item 사용할것
if (test(item) === false) { // item을 test에 전달한 결과가 false라면
result.push(item); // result에 넣어줌
}
})
return result;
};

 

 

 

 

 

 

 

_.uniq 

더보기
// _.uniq는 주어진 배열의 요소가 중복되지 않도록 새로운 배열을 리턴합니다.
// 중복 여부의 판단은 엄격한 동치 연산(strict equality, ===)을 사용해야 합니다.
// 입력으로 전달되는 배열의 요소는 모두 primitive value라고 가정합니다.
_.uniq = function (arr) {
let result = [];
_.each(arr, function (el){ //arr의 요소를 순회하며 함수를 적용한다
if(_.indexOf(result,el) === -1){ // 결과 값에 el가 없다면
result.push(el); //el를 추가해라
}
});
return result;
};

 

 

 

 

 

 

_.map

더보기
// _.map은 iteratee(반복되는 작업)를 배열의 각 요소에 적용(apply)한 결과를 담은 새로운 배열을 리턴합니다.
// 함수의 이름에서 드러나듯이 _.map은 배열의 각 요소를 다른 것(iteratee의 결과)으로 매핑(mapping)합니다.
// _.map 함수는 매우 자주 사용됩니다.
// _.each 함수와 비슷하게 동작하지만, 각 요소에 iteratee를 적용한 결과를 리턴합니다.
_.map = function (arr, iteratee) { //map함수의 파라미터로 arr, iteratee가 있다
let result = []; //빈 배열
_.each(arr, function(el){ // 'arr의 요소들에 el을 가지고 함수를 적용하는' 익명함수
result.push(iteratee(el)); //result에 넣어준다. el을 iteratee 함수에 넣은 결과를
})
return result; //result를 리턴한다
};

map은 배열에 어떤 함수를 적용한 결과를 담은 새로운 배열을 리턴한다. 이 '어떤 함수'가 iteratee.

map 내부에서 각 요소를 순회하며 함수 iteratee에 넣어주고, 그 결과를 리턴함.

iteratee는 '요소에 적용할' 함수이고, each는 'iteratee의 인자로 각 요소들을 전달' 하는 역할을 함.

 

 

 

 

 

 

 

 

_.pluck

더보기
// _.pluck은
// 1. 객체 또는 배열을 요소로 갖는 배열과 각 요소에서 찾고자 하는 key 또는 index를 입력받아
// 2. 각 요소의 해당 값 또는 요소만을 추출하여 새로운 배열에 저장하고,
// 3. 최종적으로 새로운 배열을 리턴합니다.
// 예를 들어, 각 개인의 정보를 담은 객체를 요소로 갖는 배열을 통해서, 모든 개인의 나이만으로 구성된 별도의 배열을 만들 수 있습니다.
// 최종적으로 리턴되는 새로운 배열의 길이는 입력으로 전달되는 배열의 길이와 같아야 합니다.
// 따라서 찾고자 하는 key 또는 index를 가지고 있지 않은 요소의 경우, 추출 결과는 undefined 입니다.
// _.pluck을 _.each를 사용해 구현하면 아래와 같습니다.
// let result = [];
// _.each(arr, function (item) {
// result.push(item[keyOrIdx]);
// });
// return result;
// _.pluck은 _.map을 사용해 구현하시기 바랍니다.
_.pluck = function (arr, keyOrIdx) {
let result = [];
_.map(arr, function(el){
result.push(el[keyOrIdx]); //arr가 이중배열인 경우: el은 배열. el[index]로 배열의 요소를 결과로 보냄
}) // arr가 객체를 가진 배열인 경우: el은 객체. el[key]는 객체의 key에 할당된 value. 결과로 보내줌
return result;
};

 

 

 

 

 

_.reduce

더보기
// _.reduce는
// 1. 배열을 순회하며 각 요소에 iteratee 함수를 적용하고,
// 2. 그 결과값을 계속해서 누적(accumulate)합니다.
// 3. 최종적으로 누적된 결과값을 리턴합니다.
// 예를 들어, 배열 [1, 2, 3, 4]를 전부 더해서 10이라는 하나의 값을 리턴합니다.
// 각 요소가 처리될 때마다 누적되는 값은 차례대로 1, 1+2, 1+2+3, 1+2+3+4 입니다.
// 이처럼 _.reduce는 배열이라는 다수의 정보가 하나의 값으로 축소(응축, 환원, reduction)되기 때문에 reduce라는 이름이 붙게 된 것입니다.

 

// _.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)

 

// 아래 예제를 참고하시기 바랍니다.ilter는 te
// const numbers = [1,2,3];
// const sum = _.reduce(numbers, function(total, number){
// return total + number;
// }); // 초기 값이 주어지지 않았으므로, 초기 값은 배열의 첫 요소인 1입니다. 두 번째 요소부터 반복 작업이 시작됩니다.
// // 1 + 2 = 3; (첫 작업의 결과가 누적되어 다음 작업으로 전달됩니다.)
// // 3 + 3 = 6; (마지막 작업이므로 최종적으로 6이 리턴됩니다.)
//
// const identity = _.reduce([3, 5], function(total, number){
// return total + number * number;
// }, 2); // 초기 값이 2로 주어졌습니다. 첫 번째 요소부터 반복 작업이 시작됩니다.
// // 2 + 3 * 3 = 11; (첫 작업의 결과가 누적되어 다음 작업으로 전달됩니다.)
// // 11 + 5 * 5 = 36; (마지막 작업이므로 최종적으로 36이 리턴됩니다.)
_.reduce = function (arr, iteratee, initVal) { //reduce 함수는 arr, 반복할 함수 iteratee, 초기값을 쓴다
let accumulator = initVal; //누적값에 초기값을 넣고 시작한다.

_.each(arr, function (item, idx, src) { //arr의 item을 순회할 함수. item, index, src를 쓴다
if (initVal === undefined && idx === 0) { //초기값이 없고, 첫번째 요소라면
accumulator = item; // 누적값은 현재 item(요소).
} else { //초기값이 있거나, 첫번째 요소가 아니라면(이제 누적을 시작함)
accumulator = iteratee(accumulator, item, idx, src); //누적값, arr의 요소, idx, src에 어떤 함수를 적용한 결과를 누적값에 재할당한다. 
}
});

return accumulator; //누적값을 리턴한다
};