배열의 메서드를 학습하면서 보게 된 forEach 메서드의 폴리필을 분석하면서 forEach보다 자주 사용하게 될 map과 filter 메서드의 내부 동작에 대해서도 관심이 생겼다 🤔
이번 분석을 통해 얻고자 하는 두 가지 목표를 설정해 보았다
웹 개발에서 지원하지 않는 웹 브라우저 상의 기능을 구현하는 코드
예를 들어, map 메서드를 지원하지 않는 환경에서도 map 메서드를 사용할 수 있도록 코드를 작성하는 것이다
그렇기 때문에 MDN에서 제공하는 map의 폴리필을 분석하면 실제 map 메서드의 코드가 내부적으로 어떻게 동작하는지 분석할 수 있을 것이라 생각했다
// Array.prototype에 map이 존재하지 않을 때에만 map polyfill을 실행한다.
if (!Array.prototype.map) {
// map은 인수로 콜백함수와, 콜백함수를 호출할 때 this 바인딩할 값을 전달받는다.
Array.prototype.map = function (callback, thisArg) {
// T = 콜백함수 호출 시 this에 바인딩할 값
// A = 최종적으로 반환할 배열
// k = 인덱스로 사용할 변수
var T, A, k;
if (this == null) {
throw new TypeError(' this is null or not defined');
}
// this에 바인딩된 값(map을 호출한 배열)을 인수로 객체를 생성하고 O에 할당한다.
var O = Object(this);
// map 폴리필의 가장 생소한 부분이었는데, >>>는 부호 없는 시프트 연산자로 var len에는 O.length가 음수로 평가되더라도 양수임이 보장된다.
var len = O.length >>> 0;
// 첫 번째 인수로 전달한 콜백함수의 타입이 function이 아니라면 에러를 발생시킨다.
if (typeof callback !== 'function') {
throw new TypeError(callback + ' is not a function');
}
// map을 호출할 때 2개 이상의 인수를 전달했다면 thisArg를 전달받았으므로 T에 thisArg를 할당한다.
if (arguments.length > 1) {
T = thisArg;
}
// len 크기의 새로운 배열을 생성한다.
A = new Array(len);
k = 0;
// K를 0부터 len까지 반복문을 돌린다.
while (k < len) {
var kValue, mappedValue;
if (k in O) {
kValue = O[k];
// 콜백함수를 호출하여 T를 콜백함수의 this에 바인딩하고, 콜백함수의 인수로 O의 요소, 인덱스, O 자체를 넘겨서 반환 받은 값을 mappedValue에 저장한다.
mappedValue = callback.call(T, kValue, k, O);
// 반환받은 값을 새로 생성한 A의 k 인덱스에 저장한다.
A[k] = mappedValue;
}
k++;
}
// 반복문을 모두 돌고 배열 A를 반환한다.
return A;
};
}
if (!Array.prototype.filter){
Array.prototype.filter = function(func, thisArg) {
'use strict';
// 콜백함수의 타입이 function이 아니거나 this가 falsy한 값이라면 타입에러를 발생시킨다.
if ( ! ((typeof func === 'Function' || typeof func === 'function') && this) )
throw new TypeError()
// map 메서드와 동일하게 배열 길이를 저장하기 위한 변수인 len은 0이나 양수를 보장한다.
var len = this.length >>> 0,
// 최종 반환할 배열과 필요한 변수들을 초기화하였다.
res = new Array(len),
t = this, c = 0, i = -1;
// thisArg를 전달받은 경우와 전달받지 못한 경우로 구분해서 반복문을 실행시켰다.
if (thisArg === undefined){
while (++i !== len){
if (i in this){
// 콜백함수에 인수로 요소, 인덱스, filter를 호출한 객체를 넘기며 실행시켜서 값이 true가 반환될 때만 최종반환할 배열에 요소를 저장한다.
if (func(t[i], i, t)){
res[c++] = t[i];
}
}
}
}
else{
while (++i !== len){
if (i in this){
if (func.call(thisArg, t[i], i, t)){
res[c++] = t[i];
}
}
}
}
// 처음에 반환 배열을 생성할 때 filter 메서드를 호출한 배열의 크기로 생성했기 때문에 반환 배열에 저장된 요소 개수로 length 값을 맞추고 반환한다.
res.length = c;
return res;
};
}
폴리필을 분석하기 전에 정했던 2가지 목표를 달성하고, 이와 별개로 새롭게 느낀 것들이 있었다.
this.length >>> 0
처럼 생소한 연산자를 제외하고는 막힘없이 코드 해석이 되었다. JS를 기본부터 학습하기 시작한지 한달 채 되지 않아서 많은 발전이 있었다고 느끼고 뿌듯했다. 이전에는 해석할 엄두도 내지 못했던 코드들이 읽히는 것을 몸으로 체감하면서 스스로 '잘 가고 있구나'하는 생각이 들었다. 다만 여기서 안주하지말고 이대로 계속 나아가자 🙃본론으로 돌아와서 결국 map과 filter는 내부에서 반복문을 사용하고, 이를 용도에 맞게 사용해서 새로운 배열을 반환하는 메서드라는 것을 알게 되었다
이전에 함수형 프로그래밍이 무엇인지 찾아보다가 영상을 하나 보게 되었는데, 이를 파이프라인에 비유하여 인풋을 넣으면 처리과정을 거쳐서 아웃풋이 나오게 되고, 외부에서는 파이프라인 내부의 상태를 보거나 접근할 수 없다고 했다. 그 때의 파이프라인과 이번 map, filter 메서드가 매칭되면서 머릿속으로 그림이 그려졌다.
결론적으로 map과 filter 메서드는 내부에서 반복문을 사용하여 로직을 구현하지만 이를 추상화하여 한 줄로 간편하게 사용할 수 있고, 우리는 이 내부를 들여다보지 않아도 된다(이번에는 학습 차원에서 들여다봤지만). 이러한 함수와 함수들을 묶어서(메서드체이닝) 프로그램을 구성해나가 함수형 프로그래밍에 한 발짝 다가갈 수 있다.
reference
https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Array/map
https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Array/filter