Week2. 함수형 자바스크립트와 map, filter, reduce
const products = [
{name: '반팔티', price: 15000},
{name: '긴팔티', price: 20000},
{name: '원피스', price: 25000}
];
const map = () => {
const names = [];
for (const value of products) {
names.push(products.name);
}
console.log(names); // ❌ names를 변화를 일으키는 메서드나 함수에 보내지 않음.
return names; // 🙆♀️리턴된 값을 이후에 사용하도록 함.
}
함수형 프로그래밍에선 인자와, 리턴 값
으로 소통한다.
내부의 변수를 console.log
와 같이 변화를 일으키는 함수나 메서드에 보내지 않고,
return
을 통해 리턴된 값을 개발자가 이후에 활용하도록 한다.
const products = [
{name: '반팔티', price: 15000},
{name: '긴팔티', price: 20000},
{name: '원피스', price: 25000}
];
const map = (func, iterator) => {
const names = [];
for (const value of iterator) {
names.push(func(value)); // 어떤 값을 수집할 것인지 func 함수에게 위임함.
}
return names;
}
console.log(map(value => value.name, products)); // [반팔티, 긴팔티, 원피스]
인자를 iterator라 한 이유는 products가 일전에 말한 iterable / iterator 프로토콜
을 따르는 배열이기 때문에 iterator라 표현하였다.
함수형 프로그래밍에선 명령형 프로그래밍에서처럼 하나하나 로직을 설명하지 않는다. 함수에게 해당 역할을 위임해 추상화한다. 위 예시에서 product.name을 조회한 것과 달리 해당 역할은 func
함수가 인자로 iterator의 value를 받아 return 하는 식으로 처리할 것이다.
또한 여기서 map 함수는 고차함수이다.
함수를 값으로 다루고 원하는 시점에 인자를 적용하기 때문이다.
map이 호출되어 최종 값을 출력하는 과정을 순서대로 설명해보겠다.
- map이 호출되면 인자로
value => value.name, products
를 각각 func와 iterator에 전달한다.
func
=value => value.name
,iterator
=products
for of
문을 돌면서 iterator의 value 들을 하나씩 조회한다.
첫번째 순회만 예시로 들어본다면,
value = {name: '반팔티', price: 15000}
이고func
의 인자로value
를 넘기며func
함수가 호출된다.value.name = 반팔티
가 리턴되고 names 변수에 반팔티를 push 한다.- map을 통해 최종적으로
[반팔티, 긴팔티, 원피스]
가 콘솔에 찍힐 것이다.
nodeList를 map에 담아 배열로 출력할 수 있을까?
const nodeList = document.querySelectorAll('*'); // [html, head, meta, meta, title, link, script, body, script]
console.log(nodeList.map(element => element))) // nodeList.map is not a function
nodeList
는 배열처럼 보이지만 배열이 아니다.
nodeList
는 array를 상속받은 객체가 아니고, nodeList
의 [[Prototype]]
에 map이 없기 때문에 map을 사용하려해도 사용할 수 없다.
하지만 위에서 구현한 map 함수를 통해 nodeList를 배열로 출력할 수 있다.
const map = (func, iterator) => {
const names = [];
for (const value of iterator) {
names.push(func(value)); // 어떤 값을 수집할 것인지 func 함수에게 위임함.
}
return names;
}
const nodeList = document.querySelectorAll('*');
console.log(map(value => value.nodeName, nodeList)) // [html, head, meta, meta, title, link, script, body, script]
map
함수는 iterable / iterator 프로토콜
을 따르는 많은 함수들을 다양하게 활용할 수 있다.
let iterator = document.querySelectorAll('*')[Symbol.iterator]();
console.log(iterator.next()) // {value: html, done: false};
console.log(iterator.next()) // {value: head, done: false};
console.log(iterator.next()) // {value: meta, done: false};
nodeList는 iterable / iterator 프로토콜
을 따르고 있다. 또한 map 함수는 내부적으로 iterable / iterator 프로토콜
을 따르는 for of
문을 사용하기 때문에 iterator 쪽에 iterable / iterator 프로토콜
을 따르는 것들은 다 활용이 가능한 것이다.
(프로토콜 때문에 끝말잇기 하는 기분이 드네...iterable / iterator 프로토콜
이름이 너무 길어서 앞으로는 iterable 프로토콜
이라 부르겠다. 😂)
filter는 특정 조건을 만족하는 것들을 모아 배열로 return 한다.
const products = [
{name: '반팔티', price: 15000},
{name: '긴팔티', price: 20000},
{name: '원피스', price: 25000}
];
const filter = (func, iterator) => {
const arr = [];
for (const value of iterator) {
if(func(value)) arr.push(value);
}
return arr;
};
console.log(filter(value => value.price < 20000, products));
func(value)는 조건을 판별하기 때문에 boolean 값을 리턴한다.
reduce는 iterable 값을 다른 값으로 축약한다.
const nums = [1, 2, 3, 4, 5];
const reduce = (func, acculator, iterator) => {
for (const value of iterator) {
acculator = func(acculator, value);
}
return acculator;
}
console.log(reduce((a, b) => a + b, 0, nums));
👇🏻 과정 설명
func
=a, b => a + b
;
acculator
= 0;
1.func
함수가 인자로 0과 nums의 첫번째 요소 1을 전달받으면 0 + 1 값을 리턴한다.
2. 1 (0 + 1) 은 다음 순회 때acculator
가 되어 nums의 두번째 요소 2와 더해진다.
3. 순회가 끝나면 최종 누적 값인 15를 반환한다.
JS에서는 reduce에 acculator
가 없어도 작동되도록 만들어져 있다.
예를 들어 [1, 2, 3, 4, 5]
라는 배열이 있다면, 첫번째 순회 때 1 [2, 3, 4, 5]
의 형태 (1을 꺼내 기본 값으로 설정)로 작동된다.
그럼 acculator
를 옵셔널하게 사용할 수 있도록 코드를 수정해보자.
const nums = [1, 2, 3, 4, 5];
const reduce = (func, acculator, iterator) => {
if (!iterator) { // acculator 자리가 iterator로 당겨졌기 때문에 iterator가 없는거임.
iterator = acculator[Symbol.iterator](); // 해당 객체에서 iterator를 반환하도록 함.
acculator = iterator.next().value;
}
for (const value of iterator) {
acculator = func(acculator, value);
}
return acculator;
}
console.log(reduce((a, b) => a + b, 0, nums));
console.log(reduce((a, b) => a + b, nums)); // acculator가 없을 경우
acculator
가 생략되면 기존 iterator
가 acculator
자리로 당겨지고 iterator
가 없어지게 된다. 따라서 (진짜 생략된 것은 acculator
이지만) iterator
의 존재 여부를 체크해 없을 경우, acculator
가 iterator를 반환해 iterator가 원래 역할을 할 수 있게 만들고, acculator
는 iterator의 next
메서드 속 value가 되도록 한다.
👇🏻 과정 설명
acculator가 없어져 iterator가 앞으로 당겨졌을 경우
1.iterator
존재 여부 체크해 iterator 변수에iterator
를 반환.
2.acculator
는iterator.next().value = 1
이 됨.
3. 이때 next() 메서드를 사용했으므로 iterator는 2번째 요소를 가리키는 상태임.
{value: 2, done: false}
4. 반복문을 시작하며 1과 2를 더함.
5. 순회가 끝난 후, 최종값 15 리턴
const products = [
{name: '반팔티', price: 15000},
{name: '긴팔티', price: 20000},
{name: '원피스', price: 25000},
{name: '후드티', price: 30000}
];
// 가격을 뽑아 map 만들기
const map = (func, iterator) => {
const arr = [];
for (const value of iterator) {
arr.push(func(value))
}
return arr;
}
console.log(map(value => value.price, products));
// [15000, 20000, 25000, 30000];
// 3만원 미만 상품들의 가격만 배열로 만들기
const filter = (func, iterator) => {
const arr = [];
for (const value of iterator) {
if (func(value)) arr.push(value);
}
return arr;
}
console.log(map(value => value.price,
filter(value => value.price < 30000, products)));
// [15000, 20000, 25000];
// 3만원 미만의 상품들의 가격을 다 합치기
const reduce = (func, acculator, iterator) => {
for (const {name, price} of iterator) {
acculator = func(acculator, price);
}
return acculator;
}
const add = (a, b) => a + b;
console.log(reduce(
add,
0,
map(value => value.price,
filter(value => value.price < 30000, products))));
// 60000;
함수형 프로그래밍에서 추상화에 대해 이론적으로만 이해하고 실제 구현은 어떤 식으로 이루어지는지 궁금했는데 오늘 이해한 것 같다.
보조 함수를 만들어 보조 함수가 해당 기능을 수행하도록 위임해 내부 로직에 대해 알 필요가 없다는 점? 또 함수가 값으로 활용되기 때문에 조합하기 좋아 이리저리 다른 함수와 조합한다는 점?이 있겠다.
아직까지는 기존의 map, filter, reduce가 어떤 방식으로 작동되는지 알았기 때문에 이해하기 쉬웠다면 앞으로 go와 pipe를 활용해보며 점점 심화단계로 나아갈 것 같다 🔥