map, filter, reduce의 함수는 배열의 각 요소에 대해서만 사용가능한 메서드이다. 하지만 자바스크립트가 운용되는 다양한 환경에서는 Web Apis의 querySelectorAll 메서드와 같이 일부 메서드는 iterable protocol를 따르고 있다.
따라서 map, filter, reduce와 비슷한 기능을 하면서 iterable protocol을 따르는 함수로 추상화하여 구현한다면 보다 유용하게 사용할 수 있으며 다른 함수와 조합하여 사용하기 유용해진다.
다음과 같은 배열목록이 존재한다고 가정해보자.
const products = [
{name: '티1', price: 15000},
{name: '티2', price: 20000},
{name: '티3', price: 15000},
{name: '티4', price: 30000},
{name: '티5', price: 25000}
];
특정 가격 초과, 미만의 목록만을 가져온다고 한다면 아래와 같이 구현할 수 있다.
let under20000 = [];
for(const p of products) {
if(p.price < 20000) under20000.push(p);
}
let over20000 = [];
for(const p of products) {
if(p.price > 20000) over20000.push(p);
}
iterable protocol을 따르는 함수로 추상화하여 작성한다면 아래와 같이 선언적인 방식으로 작성할 수 있다.
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, products));
console.log(filter(p => p.price > 20000, products));
reduce를 추상화한다고 하면 initialValue 인자가 없는 조건을 처리해줘야 한다.
const nums = [1,2,3,4,5];
const reduce = (f, acc, iter) => {
if(!iter) { // 두번째 인자로 초깃값이 없다면 iter의 첫번째 값을 초기값으로
iter = acc[Symbol.iterator]();
acc = iter.next().value;
}
for(const a of iter) {
acc = f(acc, a);
}
return acc;
}
console.log(reduce((a,b) => a + b, nums));
console.log(reduce((a,b) => a + b.price, 0, products));
console.log(
reduce(
(a, b) => a + b,
map(p => p.price,
filter(p => p.price > 20000, products))));
이전에는 NodeList와 같은 컬렉션을 reduce 함수를 사용하기 위해 배열로 변환한 다음 사용해야 했는데 이제는 iterable protocol을 따르는 함수로 추상화하다보니 아래와 같은 변환작업없이도 사용이 가능해졌다는 장점이 있었고 앞으로 다루게 될 코드의 조합성에 있어서 유리할 것 같은 느낌을 많이 받고 있다.
[…document.querySelectorAll('*')]
다만, 코드가 중첩되어 있다보니 가독성이 좋지 않다는 생각이 든다.
+
이후에 나올 내용에서 가독성 문제를 해결하는 것 같다.