1. reduce
reduce(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T): T;
reduce(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T, initialValue: T): T;
reduce<U>(callbackfn: (previousValue: U, currentValue: T, currentIndex: number, array: T[]) => U, initialValue: U): U;
reduce 함수는 배열을 순회하면서 콜백 함수를 반복 실행하고, 모든 반복의 결과를 하나의 값으로 줄이는(reduce) 함수다.
- callbackfn : 배열을 순회하면서 실행할 함수. 다음 네 가지 인수를 가짐.
- previousValue (=accumulator)
이전 콜백의 반환 값(=콜백의 반환 값 누적). 콜백의 첫 번째 호출이면서 initialValue를 제공한 경우에는 initialValue의 값임.
-
- currentValue
현재 원소의 값
-
- currentIndex
현재 원소의 인덱스. initialValue를 제공한 경우 0, 아니면 1부터 시작
-
- array
현재 순회하고 있는 배열 전체. reduce를 실행한 배열
- initialValue : 콜백의 최초 실행에서 첫 번째 인수 previousValue에 제공하는 값
- 이 값을 제공하지 않으면 배열의 첫 번째 원소의 값을 초기값으로 사용하고, 0번 원소의 순회를 건너뛰고 1번 원소부터 콜백 함수를 실행함
- initialValue, previousValue, reduce의 최종 리턴 값. 세 값의 타입은 모두 같음
- 빈 배열에서 초기값 없이 reduce를 실행하면 에러 발생
2. reduce를 쓰는 이유
filter, some, every 등의 기존 Array 함수는 사용할 수 있는 범위가 정해져 있다.
reduce는 이 함수들로 구현하기 까다로운 기능, 또는 다른 Array 함수가 할 수 있는 모든 것들을 reduce로 구현할 수 있다.
즉, reduce가 모든 Array 함수들의 아버지라고 할 수 있다.
3. 예시
(1) 배열 원소들의 합계 구하기
let input = [1,2,3,4,5];
let output1 = input.reduce((prev, curr) => { console.log("output1"); return prev + curr }); // 15
let output2 = input.reduce((prev, curr) => { console.log("output2"); return prev + curr }, 10); // 25
콜백 함수에 로그 출력을 굳이 넣은 이유는 "output1"은 네 번 출력되고, "output2"는 다섯 번 출력된다.
initialValue를 제공하지 않으면 0번 원소를 건너뛰는 reduce의 특징을 확인할 수 있다.
let output = 0;
input.forEach(x => output += x);
이 정도는 forEach 함수를 통해 더 쉽게 구현할 수 있다.
(2) 음수와 양수 개수 세기
let input = [-1,0,8,3,6,-11,33];
let output = input.reduce((prev, curr) => {
if (curr < 0) prev[0]++;
else if (curr > 0) prev[1]++;
return prev;
}, [0,0]); // [2,4]
initialValue로 제공한 [0,0] 배열을 초기값으로 해서 음수 개수, 양수 개수를 카운팅 할 수 있다.
let input = [-1,0,8,3,6,-11,33];
let output = [0,0];
input.forEach(x => {
if (x < 0) output[0]++;
else if (x > 0) output[1]++;
})
이 역시 forEach를 통해 똑같이 구현하는데 문제가 없는듯하다.
사실 이런 간단한 예제는 대부분 forEach, map, filter 셋 중 하나로 더 짧은 코드로 구현이 가능하다.
(3) reduce vs. filter + map
let input = [1,2,3,4,5];
let output1 = input.filter(x => x % 2 == 0); // [2,4]
let output2 = input.map(x => x * 2); // [2,4,6,8,10]
output1은 입력 배열에서 짝수인 원소만 담은 배열이고
output2는 배열의 모든 원소를 2배 곱한 배열이다. 각각 filter와 map을 이용해 쉽게 구현할 수 있다.
그렇다면 '짝수인 원소들만 골라서 2배 곱한 배열'을 리턴해야 한다면?
filter나 map 중 하나만 사용해서 짧은 코드로 구현이 가능할까?
let input = [1,2,3,4,5];
let output = input.filter(x => x % 2 == 0) // [2,4]
.map(x => x * 2); // [4,8]
마땅히 방법이 없는 듯하다. filter는 입력 배열에 존재하는 원소만 리턴 배열에 추가할 수 있고
map은 조건에 맞지 않는 원소는 추가하지 않고 continue 시키는 방법이 없다는 한계들이 있기 때문이다.
결국 filter를 통해서 리턴 받은 배열을 다시 map의 입력 배열로 사용해야 한다.
이 경우 2번의 배열 순회가 일어난다.
let input = [1,2,3,4,5];
let output = input.reduce((prev, curr) => {
if (curr % 2 == 0) prev.push(curr * 2);
return prev;
}, []);
reduce를 사용해서 한 번의 순회를 하고 동일한 결과 값을 얻을 수 있다.
let input = [1,2,3,4,5];
let output = [];
input.forEach(x => {
if (x % 2 == 0) output.push(x * 2);
});
그럼에도 불구하고 forEach로도 한 번만 순회하고 reduce와 동일한 결과값을 얻을 수 있다.
이 쯤되면 reduce와 forEach의 차이가 궁금해진다.
4. reduce vs. forEach
(1) 재사용
let reducer = function(prev, curr) {
if (curr % 2 == 0) prev.push(curr * 2);
return prev;
}
let output1 = [1,2,3,4,5].reduce(reducer, []); // [4,8]
let output2 = [6,7,8,9,10].reduce(reducer, []); // [12,16,20]
reduce 내부에 들어갈 누산 규칙을 따로 모듈화 시켜서 입력 배열만 바꿔가면서 재사용하기 편하다. 직관적이다.
let input = [1,2,3,4,5];
let output = [];
let each = function(x) {
if (x % 2 == 0) output.push(x * 2);
}
input.forEach(each);
반면 위와 비슷한 방식으로 forEach를 재사용하려면 필연적으로 output 선언이 먼저 필요하다.
이 경우 사실상 재사용이라고 부를 수 없다.
let func = function(input, output) {
input.forEach(x => {
if (x % 2 == 0) output.push(x * 2)
});
return output;
}
let output1 = func([1,2,3,4,5], []);
let output2 = func([6,7,8,9,10], []);
위를 다시 개선하면 forEach는 이렇게 재사용할 수 있다.
모듈화 시킨 범위를 forEach 안에 들어가는 함수보다 더 큰, forEach 자체를 포함하도록 확장한 것이다.
forEach뿐만 아니라 filter, map 등의 다른 Array 함수도 재사용하려면 위와 같이 사용해야 한다.
결론은 재사용 측면에서는 reduce와 forEach와 차이가 존재하며 reduce가 조금 더 사용하기 간편해 보인다.
(2) 리팩토링
리팩토링, 그리고 가독성 향상의 여러 방법 중에는 함수를 본래의 목적에 적합하게 사용하는 것이 포함돼있다.
비록 map, filter, reduce으로 구현한 기능을 forEach로 구현할 수 있다고 해도 각 함수마다 명시하는 역할이 있다.
- forEach 메서드는 주어진 함수를 배열 요소 각각에 대해 실행한다.
forEach는 "실행" 하는 의미를 갖고 있기 때문에, 실행하는 경우(로그, API 호출 등)에 적합하다.
- filter 메서드는 주어진 함수의 테스트를 통과하는 모든 요소를 모아 새로운 배열로 반환한다.
filter는 "테스트" 하는 의미를 갖고 있기 때문에, 특정 조건에 해당하는 요소를 뽑아내는 경우(a>N 등 if문)에 적합하다.
- map 메서드는 배열 내의 모든 요소 각각에 대하여 주어진 함수를 호출한 결과를 모아 새로운 배열을 반환한다.
map은 "변환" 하는 의미를 갖고 있기 때문에, 각 요소를 변환하는 경우(a * N 등)에 적합하다.
- reduce 메서드는 배열의 각 요소에 대해 주어진 리듀서(reducer) 함수를 실행하고, 하나의 결과 값을 반환한다.
다른 함수들이 배열로 반환하는 것에 비해, reduce는 단일 값을 반환해야 하는 경우(SUM 등)에 적합하다.
각 함수의 이러한 성격들을 잘 이해하고 알맞게 쓸 필요가 있는 것 같다.
'JavaScript 기본' 카테고리의 다른 글
Array 함수 #6. 기타 (0) | 2022.03.08 |
---|---|
Array 함수 #5. sort (0) | 2022.03.03 |
Array 함수 #3. 배열에 원소 추가,삭제 (2) | 2022.02.25 |
Array 함수 #2. 조건 확인 (0) | 2022.02.25 |
Array 함수 #1. 반복문 (0) | 2022.01.07 |