[functional JS ES6+] map, filter, reduce
작성:    
업데이트:
카테고리: Functional JS
태그: FE Language, Functional JS, JS
본 포스트는 인프런의 함수형 프로그래밍과 JavaScript ES6+ 강의(링크)를 듣고 정리한 내용입니다.
map
map 함수?
불필요한 for문을 제거하면서 동일한 기능을 하게 한다
products = [
{name: '바지', price: 25000},
{name: '반팔티', price: 15000},
{name: '긴팔티', price: 20000},
]
// 기존 for 문
let names = [];
for (const p of products) {
names.push(p.name);
}
log(names); // ["바지", "반팔티", "긴팔티"]
// map 문
const map = (f, iter) => {
let res = [];
for (const a of iter) {
res.push(f(a));
}
return res;
}
log(map(p => p.name, products)); // ["바지", "반팔티", "긴팔티"]
- 인자로 iterable을 받고, 함수도 받을 수 있다.
- 함수로 iterable의 각 요소를 처리한 값들을 return에 반환
- map 함수는 함수를 인자로 다루어 함수 내부에서 사용하기 때문에 고차함수
- iterable protocol을 따르므로 높은 다형성을 가진다.
map 함수의 높은 다형성
log([1, 2, 3].map(a => a+1)); // [2, 3, 4]
log(map(el => el.nodeName, document.querySelectorAll('*'))); // ["HTML", "HEAD", "SCRIPT", ...]
document.querySelectorAll('*').map(el => el.nodeName); // TypeError
- iterable이라면 map함수를 사용할 수 있다.
- 하지만 iterable처럼 생긴 객체들에 대해 주의해야 한다.
- 예를 들면 document.querySelectorAll을 통해 반환되는 NodeList는 array처럼 생겼으나, array를 상속받은 객체가 아니어서, map함수를 내장하고 있지 않아, 특별한 정의 없이 사용할 수 없다.
- 결론 : iterable인 것이 반드시 map함수를 내장하고 있다는 것은 아니다!
filter
T/F 여부에 따라 걸러내는 함수
// 기존 명령형
const under20000 = [];
for (const p of products) {
if (p.price < 20000) under20000.push(p);
}
log(under20000); // [{name: '반팔티', price: 15000}]
// filter 함수 사용
const filter = (f, iter) => {
let res = [];
for (a of iter) {
if (f(a)) res.push(a);
}
return res;
}
log(filter(p => p.price < 20000, products)); // [{name: '반팔티', price: 15000}]
log(filter(n => n % 2, [1, 2, 3, 4])); // [1, 3]
log(filter(n => n % 2, function *() {
yield 1;
yield 2;
yield 3;
yield 4;
yield 5;
})) // [1, 3, 5]
- filter 함수 역시 iterable protocol을 따르는 iterable을 조작 가능
reduce
iterable을 하나의 값으로 축약하는 함수
// nums를 모두 더하는 경우
const nums = [1, 2, 3, 4, 5];
// 기존 방식
let total = 0;
for (const n of nums) {
total += n;
}
log(total); // 15
// reduce 방식
const reduce = (f, init, iter) => {
acc = init;
for (const a of iter) {
acc = f(acc, a);
}
return acc;
}
const add = (a, b) => a + b;
log(reduce(add, 0, nums));
- iterable을 모두 돌면서 인자로 주어지는 함수 f를 누적값과의 연산으로 처리
- 최종적으로 모두 iterable을 돌면서 마무리되는 acc 값을 return
- JS 내장 reduce는 init(초기값)을 생략하는 경우 iter의 첫번째 요소를 init으로 사용하도록 자동 설정
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;
}
reduce 질문 내용
이 과정에서 이해가 되지 않아 질문을 올렸다가 이해해버려서 정정한 질문을 남긴다
안녕하세요! 초기값 acc가 없는 경우 iter에서 첫 값을 acc로 지정하고 next() 메서드를 통해 두번째 값부터 f(acc, a)를 누적하는 로직에서 궁금한 점이 있어 질문 남깁니다.
인자에 acc가 부재한 경우 if(!iter) 조건이 아니라 if(!acc) 조건으로 iter의 첫 값을 acc에 지정해주는 게 맞지 않나 싶었습니다. 해당 부분 시작 전에도 JS 내장 reduce 방식처럼 acc가 없는 경우 사용하는 방식이라고 소개하셔서 코드 부분에서 더 괴리가 있는 것 같습니다.
또한 acc[Symbol.iterator]();
의 경우 acc는 초기값이고, iterator 프로토콜을 따르는 배열은 인자로 주어진 iter니까 iter[Symbol.iterator]()
가 맞지 않나요?
const reduce = (f, acc, iter) => {
if (!iter) {
iter = acc[Symbol.iterator]();
acc = iter.next().value;
}
...
}
강의에서는 위와 같이 작성해주셨는데, 제가 생각했을 때 이해가 되는 코드는 아래와 같습니다.
const reduce = (f, iter, acc) => {
if (!acc) {
iter = iter[Symbol.iterator]();
acc = iter.next().value;
}
...
}
acc가 없는 경우 인자가 2개가 들어온다고 생각하면, acc를 3번째 인자로 두어야 acc가 undefined라 !acc가 true 처리되어서 if문을 돌 것 같아서 순서를 바꿔보았습니다.
제가 배움이 얕아 잘못 생각하고 있는 것이라면 어떤 부분에서 잘못 생각하고 있는건지 여쭙고 싶습니다. 감사합니다.
__
라고 생각했는데, 아예 iter랑 acc를 재지정해주셨다는 걸 알게 되었습니다. 다른 수강생들에게도 도움이 되기 위해 정리해보는데, 혹시 오류가 있다면 짚어주시면 감사하겠습니다.
초기값(acc)이 함수로 전달되지 않는 경우 인자가 당겨져서 if 문 이전까지는 acc가 iterable(배열)이고, iter가 부재해undefined가 됩니다. 그러면 초기값이 없다는 if 문의 판별 조건은 (!iter)가 됩니다.
if문 내부에서는 acc가 iterable이니 이를 iter로 다시 지정하는 것이고, acc는 단어 뜻 그대로 초기값으로 설정하기 위해 iter.next().value로 iterable의 첫 값으로 지정합니다.
next()를 해서 iterable은 두 번째 값부터 for문에서 iteration을 진행하게 됩니다.
iter와 acc 단어 자체의 의미에 집중하다보니 이해가 되지 않았는데, 당겨진다고 생각하니까 바로 납득이 되네요. 오류가 있다면 지적 부탁드립니다. 감사합니다!
댓글남기기