map
, filter
, some
, every
, find
같은 모든 배열의 고차 함수는 reduce
메서드로 구현할 수 있다고 한다. 나는 해봐야 알 것 같다.
구현은 고차 함수들의 사용법을 그대로 유지하기 위해 Array.prototype
에 구현한 함수들을 프로토타입 메서드로 추가하는 방식을 택했다. (프로토타입 확장)
가장 먼저 map 함수를 구현해보려고 한다.
map 함수의 정의는 다음과 같은 형식을 따른다.
arr.map(callback(currentValue[, index[, array]])[, thisArg])
map 메서드는 배열 내의 모든 요소 각각에 대해 주어진 함수를 호출한 결과를 모아 새로운 배열을 반환한다.
map 메서드가 생성하여 반환하는 새로운 배열의 length 프로퍼티 값은 map 메서드를 호출한 배열의 length 프로퍼티 값과 반드시 일치한다. 즉, map 메서드를 호출한 배열과 map 메서드가 생성하여 반환한 배열은 1:1 매핑한다.
if (!Array.prototype.mapUsingReduce) {
Array.prototype.mapUsingReduce = function (callback, thisArg) {
return this.reduce((mapped, cur, idx, arr) => {
mapped[idx] = callback.call(thisArg, cur, idx, arr);
return mapped;
}, []);
};
}
console.log([1, 2, 3, 4, 5].mapUsingReduce((item) => item * 2)); // [2, 4, 6, 8, 10]
map 함수의 정의를 보면, 두 번째 인자로 thisArg가 전달되는 경우 해당 값으로 callback을 실행할 때 this 바인딩해주어야 한다. 따라서 mapUsingReduce에 전달되는 콜백함수는 thisArg에 this 바인딩을 해줄 필요가 있다. (call
)
callback.call(thisArg, cur, idx, arr);
⚠️ 주의
// ❌ if (!Array.prototype.mapUsingReduce) { Array.prototype.mapUsingReduce = (callback, thisArg) => { return this.reduce((mapped, cur, idx, arr) => { // TypeError: this.reduce is not a function mapped[idx] = callback.call(thisArg, cur, idx, arr); return mapped; }, []); }; } console.log([1, 2, 3, 4, 5].mapUsingReduce((item) => item * 2)); // [2, 4, 6, 8, 10]
처음 코드를 짤 때
Array.prototype.mapUsingReduce
의 함수 정의를 화살표 함수를 이용했다. 그 결과TypeError: this.reduce is not a function
가 발생했고, this가 GlobalObject를 가리키고 있음을 확인했다.
이는 화살표 함수는 함수 자체의 this 바인딩을 갖지 않기 때문에 발생한 문제다. 즉, 화살표 함수 내부에서 this를 참조하면 상위 스코프인 전역의 this, globalObject를 그대로 참조하게 된다.
따라서 위의 에러를 해결하기 위해서는 화살표 함수 대신 일반 함수를 사용해야 했다. 이에 주의하여 나머지 고차 함수들도 구현해보자.
arr.forEach(callback(currentvalue[, index[, array]])[, thisArg])
forEach
메서드는 주어진 함수를 배열 요소 각각에 대해 실행한다.
forEach
는 언제나 undefined
를 반환한다.
if (!Array.prototype.forEachUsingReduce) {
Array.prototype.forEachUsingReduce = function (callback, thisArg) {
return this.reduce((_, cur, idx, arr) => {
callback.call(thisArg, cur, idx, arr);
}, null);
};
}
const numbers = [1, 2, 3];
let pows = [];
numbers.forEachUsingReduce((item) => pows.push(item ** 2));
console.log(pows); // [1, 4, 9]
map
과 거의 비슷하게 구현이 된다.
처음 구현 시에 Array.prototype.forEachUsingReduce
내부 reduce
()의 두번째 인자를 비워두었었다. 그러나 위 실행 결과가 [ 4, 9 ]가 나왔고, 이는 배열의 첫 번째 요소인 1이 누적값으로 사용되면서 콜백 함수가 호출되었기 때문이다. reduce
문의 initialValue를 제공하지 않으면 배열의 첫 번째 요소를 사용한다.
이를 해결하기 위해 null
을 사용하여 reduce
의 누적값을 무시하고 각 요소에 대해 콜백 함수를 실행하게 된다.
따라서 reduce
문 사용시 위와 같이 원치 않는 동작이 발생할 수 있기 때문에 명시적으로 초기값을 설정해주자.
arr.sort([compareFunction])
sort
메서드는 배열의 요소를 정렬한다. 원본 배열을 직접 변경하여 정렬된 배열을 반환한다. 기본 정렬 순서는 문자열의 유니코드 코드 포인트를 따른다.
sort
메서드는 quicksort 알고리즘을 사용했었다. quicksort 알고리즘은 동일한 값의 요소가 중복되어 있을 때 초기 순서와 변경될 수 있는 불안정한 정렬 알고리즘으로 알려져 있다. ES10에서는 timesort
알고리즘을 사용하도록 바뀌었다.
지금은 reduce를 학습하는 과정이기 때문에 정렬 알고리즘의 효율성은 고려하지 않았다.
if (!Array.prototype.sortUsingReduce) {
Array.prototype.sortUsingReduce = function (compareFunction) {
this.reduce((acc, cur) => {
if (!acc.length) return [cur];
let index = 0;
while (index < acc.length && compareFunction(cur, acc[index])) index++;
acc.splice(index, 0, cur);
return acc;
}, []);
};
}
const fruits = ['Banana', 'Orange', 'Apple'];
fruits.sort();
console.log(fruits); // ['Apple', 'Banana', 'Orange']
const points = [40, 100, 1, 5, 2, 25, 10];
points.sort((a, b) => a - b);
console.log(points); // [1, 2, 5, 10, 25, 40, 100]
sort에 전달되는 비교 함수의 반환 값에 따라 정렬되는 순서는 아래와 같다.
sort() 메서드는 다른 고차 함수와 달리 콜백 함수 내부에서 this로 사용할 객체를 전달하지 않는다.
arr.filter(callback(element[, index[, array]])[, thisArg])
filter
메서드는 자신을 호출한 배열의 모든 요소를 순회하면서 인수로 전달받은 콜백 함수를 반복 호출하고, 콜백 함수의 반환 값이 true인 요소로만 구성된 새로운 배열을 반환한다. 이때 원본 배열을 변경되지 않는다.
if (!Array.prototype.filterUsingReduce) {
Array.prototype.filterUsingReduce = function (callback, thisArg) {
return this.reduce((acc, cur, idx, arr) => {
if (callback.call(thisArg, cur, idx, arr)) acc = [...acc, cur];
return acc;
}, []);
};
}
const words = ['spray', 'limit', 'elite', 'exuberant', 'destruction', 'present'];
const result = words.filterUsingReduce((word) => word.length > 6);
console.log(result); // ["exuberant", "destruction", "present"]
arr.some(callbackFn(element[, index[, array]])[, thisArg])
some
메서드는 자신을 호출한 배열의 요소를 순회하면서 인수로 전달된 콜백 함수를 호출한다. 이때 some 메서드는 콜백 함수의 반환값이 단 한번이라도 참이면 true, 모두 거짓이면 false를 반환한다. 즉, 배열의 요소 중에 콜백 함수를 통해 정의한 조건을 만족하는 요소가 1개 이상 존재하는지 확인하여 그 결과를 불리언 타입으로 반환한다.
if (!Array.prototype.someUsingReduce) {
Array.prototype.someUsingReduce = function (callbackFn, thisArg) {
return this.reduce(
(hasMatch, cur, idx, arr) => hasMatch || callbackFn.call(thisArg, cur, idx, arr),
false,
);
};
}
const array = [1, 2, 3, 4, 5];
const even = (element) => element % 2 === 0;
console.log(array.someUsingReduce(even)); // true
const zero = (element) => !element;
console.log(array.someUsingReduce(zero)); // false
기존 reduce의 accumulator에 boolean 값이 저장된다. 다만 reduce
를 이용해 구현하다보니 hasMatch
가 true
여도 early-return 할 수 없고 모든 배열 내 요소들을 순회해야 some의 반환값을 얻을 수 있어 효율이 떨어진다.
arr.every(callbackFn(element[, index[, array]])[, thisArg])
every
메서드는 자신을 호출한 배열의 요소를 순회하면서 인수로 전달된 콜백 함수를 호출한다. 이때 every 메서드는 콜백 함수의 반환값이 모두 참이면 ture, 단 한번이라도 거짓이면 false를 반환한다. 즉, 배열의 모든 요소가 콜백 함수를 통해 정의한 조건을 모두 만족하는지 확인하여 그 결과를 불리언 타입으로 반환한다.
if (!Array.prototype.everyUsingReduce) {
Array.prototype.everyUsingReduce = function (callbackFn, thisArg) {
return this.reduce(
(allMatch, cur, idx, arr) => allMatch && callbackFn.call(thisArg, cur, idx, arr),
true,
);
};
}
arr.find(callback(element[, index[, array]])[, thisArg])
ES6에서 도입된 find
메서드는 자신을 호출한 배열의 요소를 순회하면서 인수로 전달된 콜백 함수를 호출하여 반환값이 true인 첫 번째 요소를 반환한다. 만약 콜백 함수의 반환값이 true인 요소가 존재하지 않는다면 undefined를 반환한다.
if (!Array.prototype.findUsingReduce) {
Array.prototype.findUsingReduce = function (callback, thisArg) {
return this.reduce((match, cur, idx, arr) => {
if (!match && callback.call(thisArg, cur, idx, arr)) return cur;
return match;
}, undefined);
};
}
const array1 = [5, 12, 8, 130, 44];
const found = array1.findUsingReduce((element) => element > 10);
console.log(found); // 12
arr.findIndex(callbackFn(element[, index[, array]])[, thisArg])
ES6에 도입된 findIndex
메서드는 자신을 호출한 배열의 요소를 순회하면서 인수로 전달된 콜백 함수를 호출하여 반환값이 true인 첫 번째 요소의 인덱스를 반환한다.
if (!Array.prototype.findIndexUsingReduce) {
Array.prototype.findIndexUsingReduce = function (callbackFn, thisArg) {
return this.reduce((match, cur, idx, arr) => {
if (!match && callbackFn.call(thisArg, cur, idx, arr)) return idx;
return match;
}, undefined);
};
}
const array1 = [5, 12, 8, 130, 44];
const found = array1.findIndexUsingReduce((element) => element > 10);
console.log(found); // 1
arr.flatMap(callback(currentValue[, index[, array]])[, thisArg])
ES10에서 도입된 flatMap
메서드는 map 메서드를 통해 생성된 새로운 배열을 평탄화한다. 즉 map
메서드와 flat
메서드를 순차적으로 실행하는 효과가 있다.
if (!Array.prototype.flatMapUsingReduce) {
Array.prototype.flatMapUsingReduce = function (callback, thisArg) {
return this.reduce((acc, cur, idx, arr) => {
const mapped = callback.call(thisArg, cur, idx, arr);
return [...acc, ...mapped];
}, []);
};
}
let arr = [1, 2, 3, 4];
console.log(arr.flatMapUsingReduce((x) => [x * 2])); // [2, 4, 6, 8]
console.log(arr.flatMapUsingReduce((x) => [[x * 2]])); // [[2], [4], [6], [8]]
여기까지 모던 자바스크립트 딥다이브 책에 나오는 배열의 주요 고차 함수들을 reduce로 구현해보았다. 그동안 자바스크립트 공부를 게을리 해왔어서 reduce 함수 사용법도 잘 몰랐었는데 이번 기회에 reduce는 물론 각 함수들의 역할까지 완벽하게 이해하고 넘어가는 것 같다. 특히 reduce로 다양한 구현을 해보면서 매우 강력한 기능을 가지고 있음을 느꼈고, 구현에 오래 걸릴 것이라 예상했는데 생각보다 어렵지 않게 구현해 볼 수 있어서 좋았다. ✨
같이 고민할 내용이나 오류가 있다면 댓글 남겨주시면 감사하겠습니다.
모던 자바스크립트 DeepDive
MDN JavaScript Array