JavaScript ES6 - 4. map, filter, reduce


유인동님의 함수형 프로그래밍과 JavaScript ES6+ 인프런 강의를 듣고 개인적으로 정리한 내용입니다.
함수형 프로그래밍과 JavaScript ES6 map, filter, reduce에 대해서 설명한다.

map

Map 객체는 요소의 삽입 순서대로 원소를 순회한다. for...of 반복문은 각 순회에서 [key, value]로 이루어진 배열을 반환한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const products = [
{name: "반팔티", price: 15000},
{name: "반팔티", price: 20000},
{name: "핸드폰케이스", price: 15000},
{name: "후드티", price: 30000},
{name: "바지", price: 25000}
];

let names = [];
for (const p of products) {
names.push(p.name);
}

console.log(names)

let prices = [];
for (const p of products) {
prices.push(p.price);
}

console.log(prices)
console
1
2
> (5) ["반팔티", "반팔티", "핸드폰케이스", "후드티", "바지"]
> (5) [15000, 20000, 15000, 30000, 25000]

map은 고차 함수이며 함수를 값으로 다루면서 원하는 시점에 인자를 적용시킨다.
아래 예제에서 products라는 이터러블에서 내가 원하는 인자를 가지고 오는 예제이다.

map은 인자와 리턴값을 사용하기를 권장한다.

1
2
3
4
5
6
7
8
9
10
11
const map = (f, iter) => {
let res = [];
for (const p of iter) {
res.push(f(p)); // 내가 수집할 인자를 받음.
}

return res;
}

console.log(map(p => p.name, products));
console.log(map(p => p.price, products));
console
1
2
> (5) ["반팔티", "반팔티", "핸드폰케이스", "후드티", "바지"]
> (5) [15000, 20000, 15000, 30000, 25000]

이터러블 프로토콜을 따른 map의 다형성

map() 메서드는 배열 내의 모든 요소 각각에 대하여 주어진 함수를 호출한 결과를 모아 새로운 배열을 반환한다.
헬퍼(Helper)함수를 사용하여 map의 다형성에 대해서 알아보자.

1
console.log(document.querySelectorAll("*").map(el => el.nodeName));
console error
1
> Uncaught TypeError: document.querySelectorAll(...).map is not a function

위에 코드와 같이 document.querySelectorAll 함수 내부에 map 함수가 없는 것을 확인 할 수 있다. 그 이유는 document 함수는 Array를 상속받은 객체가 아니기 때문에 map 함수가 프로토타입이 구현이 안되어 있다.

Array는 map을 통해서 값을 수집 할 수 있다.

1
console.log([1, 2, 3].map(a => a + 1));
console
1
> (3) [2, 3, 4]

위에서 만든map 함수를 이용해서 document.querySelectorAll("*").map(el => el.nodeName)를 사용해보자.

1
console.log(map(el => el.nodeName, document.querySelectorAll("*")));
console
1
(7) ["HTML", "HEAD", "META", "BODY", "SCRIPT", "SCRIPT", "SCRIPT"]

실행이 정상적으로 되는 이유가 document.querySelectorAll가 이터러블 프로토콜을 따르고 있기 때문이다. 이터러블을 정상적으로 따르면서 for…of문을 사용하여 순회가 가능한지 확인 해보자.

1
2
3
4
5
6
const it = document.querySelectorAll("*")[Symbol.iterator]();
console.log(it.next());
console.log(it.next());
console.log(it.next());
console.log(it.next());
console.log(it.next());
console
1
2
3
4
5
{value: html, done: false}
{value: head, done: false}
{value: meta, done: false}
{value: body, done: false}
{value: script, done: false}

이와 같이 제네레이터 함수도 이용해보면

1
2
3
4
5
6
7
function* gen() {
yield 2;
if (false) yield 3;
yield 4;
}

console.log(map(a => a * a, gen()));
console
1
(2) [4, 16]

추가적으로 new Map()을 이용해보자.

1
2
3
4
5
let m = new Map();
m.set("a", 10);
m.set("b", 20);

console.log(new Map(map(([k, a]) => [k, a * 2], m)));
console
1
> Map(2) {"a" => 20, "b" => 40}

map은 문장도 역시 사용할 수 있다.(모든 것 들을 map으로 사용할 수 있다는 것이다)
여기서 이터러블 프로토콜을 사용한다는것은 다른 헬퍼함수들과 조합성이 좋아진다.

filter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const filterProducts = [
{name: "반팔티", price: 15000},
{name: "반팔티", price: 20000},
name: "핸드폰케이스", price: 15000},
{name: "후드티", price: 30000},
{name: "바지", price: 25000}
];

let under20000 = [];
for (const p of filterProducts) {
if (p.price < 20000) {
under20000.push(p);
}
}

// 전개 연산자 사용
console.log(...under20000);
console
1
> {name: "반팔티", price: 15000} > {name: "핸드폰케이스", price: 15000}

filter() 메서드를 만들어서 적용 시키면

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const filter = (f, iter) => {
let res = [];
for (const a of iter) {
if (f(a)) {
res.push(a);
}
}

return res;
};

console.log(...filter(p => p.price < 20000, filterProducts));

console.log(filter(n => n % 2, [1, 2, 3, 4]));

console.log(filter(n => n % 2, function* () {
yield 1;
yield 2;
yield 3;
}()));
console
1
2
3
> {name: "반팔티", price: 15000} > {name: "핸드폰케이스", price: 15000}
> (2) [1, 3]
> (2) [1, 3]

reduce

reduce() 메서드는 배열의 각 요소에 대해 주어진 리듀서(reducer) 함수를 실행하고, 하나의 결과값을 반환한다.

1
2
3
4
5
6
7
8
const nums = [1, 2, 3, 4, 5];
let total = 0;

for (const n of nums) {
total = total + n;
}

console.log(total);
console
1
> 15

외부 인터페이스로 살펴보자.

1
2
3
4
5
6
7
8
9
10
11
12
const reduce = (f, acc, iter) => {
for (const a of iter) {
acc = f(acc, a);
}

return acc;
};

const add = (a, b) => a + b;

console.log(reduce(add, 0, [1, 2, 3, 4, 5]));
console.log(add(add(add(add(add(0, 1), 2), 3), 4), 5));
console
1
2
> 15
> 15

추가적으로 filter() 메서드에 acc값이 없이 사용 할 수 있도록 reduce가 구현 되어 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const reduce = (f, acc, iter) => {
if (!iter) {
iter = acc[Symbol.iterator]();
acc = iter.next().value;
}

for (const a of iter) {
acc = f(acc, a);
}

return acc;
};

const add = (a, b) => a + b;

console.log(reduce(add, [1, 2, 3, 4, 5]));
console
1
> 15

acc 값이 없으면 [1, 2, 3, 4, 5] 에서 맨 앞에 있는 1이 초기 값으로 설정된다. 그래서 2, 3, 4, 5 더한 13 의 결과 값이 나온다.

1
2
3
4
5
6
7
8
9
const reduceProducts = [
{name: "반팔티", price: 15000},
{name: "반팔티", price: 20000},
{name: "핸드폰케이스", price: 15000},
{name: "후드티", price: 30000},
{name: "바지", price: 25000}
];

console.log(reduce((totalPrice, product) => totalPrice + product.price, 0, reduceProducts));
console
1
> 105000

map+filter+reduce 중첩 사용과 함수형 사고

폴더 구조

fx.js 함수

fx.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const map = (f, iter) => {
let res = [];
for (const a of iter) {
res.push(f(a));
}
return res;
};

const filter = (f, iter) => {
let res = [];
for (const a of iter) {
if (f(a)) res.push(a);
}
return res;
};

const reduce = (f, acc, iter) => {
if (!iter) {
iter = acc[Symbol.iterator]();
acc = iter.next().value;
}
for (const a of iter) {
acc = f(acc, a);
}
return acc;
};

20000원 이하의 모든 값의 합

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const products = [
{name: '반팔티', price: 15000},
{name: '긴팔티', price: 20000},
{name: '핸드폰케이스', price: 15000},
{name: '후드티', price: 30000},
{name: '바지', price: 25000}
];


const add = (a, b) => a + b;

console.log(reduce(
add,
map(p => p.price,
filter(p => p.price < 20000, products))));

console.log(
reduce(
add,
filter(n => n >= 20000,
map(p => p.price, products))));
console
1
2
> 30000
> 30000

함수는 함수를 중첩해서 사용할수 있다. 코드를 읽을때 오른쪽에서부터 왼쪽으로 읽으면, products에 price가 20000원 이하를 필터를 하고, 해당하는 값(price)을 map으로 뽑아내고, 합하여 축약을 한다.

map, filter, reduce 소스코드

참조