유인동님의 함수형 프로그래밍과 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)
|
console1 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));
|
console1 2
| > (5) ["반팔티", "반팔티", "핸드폰케이스", "후드티", "바지"] > (5) [15000, 20000, 15000, 30000, 25000]
|
이터러블 프로토콜을 따른 map의 다형성
map()
메서드는 배열 내의 모든 요소 각각에 대하여 주어진 함수를 호출한 결과를 모아 새로운 배열을 반환한다.
헬퍼(Helper)함수를 사용하여 map의 다형성에 대해서 알아보자.
1
| console.log(document.querySelectorAll("*").map(el => el.nodeName));
|
console error1
| > 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));
|
위에서 만든map 함수를 이용해서 document.querySelectorAll("*").map(el => el.nodeName)
를 사용해보자.
1
| console.log(map(el => el.nodeName, document.querySelectorAll("*")));
|
console1
| (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());
|
console1 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()));
|
추가적으로 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)));
|
console1
| > 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);
|
console1
| > {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; }()));
|
console1 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);
|
외부 인터페이스로 살펴보자.
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));
|
추가적으로 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]));
|
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));
|
map+filter+reduce 중첩 사용과 함수형 사고
폴더 구조
fx.js 함수
fx.js1 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))));
|
함수는 함수를 중첩해서 사용할수 있다. 코드를 읽을때 오른쪽에서부터 왼쪽으로 읽으면, products에 price가 20000원 이하를 필터를 하고, 해당하는 값(price)을 map으로 뽑아내고, 합하여 축약을 한다.
map, filter, reduce 소스코드
참조