map()
, filter()
, reduce()
를 활용해 배열 반복문을 함수형 도구로 바꾸는 방법에 대해서 기술한 장이다.
Map()
Map 함수 만들기
// Map 함수 만들기
fuction map(arr, f) {
let newArr = [];
forEach(array, function(e) {
newArr.push(f(e));
});
return newArr;
}
배열과 함수를 인자로 받아 내부에 빈 배열을 만들고 원래 배열 항목으로 새로운 항목을 만들기 위해 f() 함수를 부른다. 이후에 원래 배열 항목에 해당하는 새로운 항목을 추가하고 새 배열을 반환한다.
cf) JavaScript map()
polyfill
// Production steps of ECMA-262, Edition 5, 15.4.4.19
// Reference: http://es5.github.io/#x15.4.4.19
if (!Array.prototype.map) {
Array.prototype.map = function (callback, thisArg) {
var T, A, k;
if (this == null) {
throw new TypeError(" this is null or not defined");
}
// 1. Let O be the result of calling ToObject passing the |this|
// value as the argument.
var O = Object(this);
// 2. Let lenValue be the result of calling the Get internal
// method of O with the argument "length".
// 3. Let len be ToUint32(lenValue).
var len = O.length >>> 0;
// 4. If IsCallable(callback) is false, throw a TypeError exception.
// See: http://es5.github.com/#x9.11
if (typeof callback !== "function") {
throw new TypeError(callback + " is not a function");
}
// 5. If thisArg was supplied, let T be thisArg; else let T be undefined.
if (arguments.length > 1) {
T = thisArg;
}
// 6. Let A be a new array created as if by the expression new Array(len)
// where Array is the standard built-in constructor with that name and
// len is the value of len.
A = new Array(len);
// 7. Let k be 0
k = 0;
// 8. Repeat, while k < len
while (k < len) {
var kValue, mappedValue;
// a. Let Pk be ToString(k).
// This is implicit for LHS operands of the in operator
// b. Let kPresent be the result of calling the HasProperty internal
// method of O with argument Pk.
// This step can be combined with c
// c. If kPresent is true, then
if (k in O) {
// i. Let kValue be the result of calling the Get internal
// method of O with argument Pk.
kValue = O[k];
// ii. Let mappedValue be the result of calling the Call internal
// method of callback with T as the this value and argument
// list containing kValue, k, and O.
mappedValue = callback.call(T, kValue, k, O);
// iii. Call the DefineOwnProperty internal method of A with arguments
// Pk, Property Descriptor
// { Value: mappedValue,
// Writable: true,
// Enumerable: true,
// Configurable: true },
// and false.
// In browsers that support Object.defineProperty, use the following:
// Object.defineProperty(A, k, {
// value: mappedValue,
// writable: true,
// enumerable: true,
// configurable: true
// });
// For best browser support, use the following:
A[k] = mappedValue;
}
// d. Increase k by 1.
k++;
}
// 9. return A
return A;
};
}
이 함수는 값 하나를 바꾸는 함수를 배열 전체를 바꾸는 데 사용할 수 있다. 다만 map()
에 액션을 넘기게 되면 모든 항목이 액션이 되므로, 계산에서 사용하는 것이 바람직하다.
인라인 함수(inline function)
함수 이름을 쓰는 대신 쓰는 곳에서 무명으로 바로 정의하는 함수로, 익명 함수라고도 할 수 있다.
Filter()
Filter 함수 만들기
function filter(arr, f) {
let newArr = [];
forEach(arr, function(e) {
if(f(e)) newArr.push(e);
});
return newArr;
}
배열과 함수를 인자로 받아 지역적으로 빈 배열을 만들고, f()를 호출해 결과 배열에 넣을지 확인한다. 조건에 맞을 때만 원래 항목을 결과 배열에 넣고 새 배열을 반환한다.
cf) JavaScript filter()
polyfill
// ('../internals/array-iteration').filter;
var createMethod = function (TYPE) {
var IS_MAP = TYPE === 1;
var IS_FILTER = TYPE === 2;
var IS_SOME = TYPE === 3;
var IS_EVERY = TYPE === 4;
var IS_FIND_INDEX = TYPE === 6;
var IS_FILTER_REJECT = TYPE === 7;
var NO_HOLES = TYPE === 5 || IS_FIND_INDEX;
return function ($this, callbackfn, that, specificCreate) {
var O = toObject($this);
var self = IndexedObject(O);
var boundFunction = bind(callbackfn, that);
var length = lengthOfArrayLike(self);
var index = 0;
var create = specificCreate || arraySpeciesCreate;
var target = IS_MAP ? create($this, length) : IS_FILTER || IS_FILTER_REJECT ? create($this, 0) : undefined;
var value, result;
for (;length > index; index++) if (NO_HOLES || index in self) {
value = self[index];
result = boundFunction(value, index, O);
if (TYPE) {
if (IS_MAP) target[index] = result; // map
else if (result) switch (TYPE) {
case 3: return true; // some
case 5: return value; // find
case 6: return index; // findIndex
case 2: push(target, value); // filter
} else switch (TYPE) {
case 4: return false; // every
case 7: push(target, value); // filterReject
}
}
}
return IS_FIND_INDEX ? -1 : IS_SOME || IS_EVERY ? IS_EVERY : target;
};
};
var $filter = createMethod(2); // require('../internals/array-iteration').filter;
function filter(callbackfn /* , thisArg */) {
return $filter(this, callbackfn, arguments.length > 1 ? arguments[1] : undefined);
}
filter()
함수의 polyfill은 찾기 힘들었다😂 MDN에도 링크만 걸려있고, 모듈을 왕창 뒤져야 찾을 수 있었는데 내부 구현은 책에서 한 것과 거의 비슷했다. 인자의 갯수만 좀 다를 뿐이었다. 더불어서 이전 장에서 봤던 고차함수를 활용해 반환 값으로 함수를 뱉어내서 여러 함수를 만드는 생성 함수를 하나의 메소드로 만드는 함수의 예시이기도 했다.
술어(predicate)
true, false를 반환하는 함수로,filter()
나 다른 고차 함수에 전달하기 좋다.
Reduce()
reduce()
함수는 누적 계산을 하기 위한 함수형 도구로 이전 함수들과는 다른 특성을 가지고 있다. 단순 연산의 개념뿐만이 아니라 객체나 문자열을 합치는 데에도 활용이 가능하다.
reduce()
함수를 사용할 때 유의할 사항 두 가지는 인자의 순서와 초깃값을 결정하는 방법이라고 되어있으나 보통 이미 만들어진 메소드를 쓰기 때문에 mdn을 잘 따라보자.
function reduce(arr, init, f) {
let acc = init;
forEach(arr, function(e) {
acc = f(acc, e);
});
return acc;
}
배열과 초깃값, 누적 함수를 매개변수로 담아 처음엔 누적값을 초깃값으로 초기화해준다. 그러고 난 뒤 누적 값 계산을 위해 현재 값과 배열 항목으로 f() 함수를 호출하고 누적된 값을 반환한다.
// reduce()로 map() 만들기
if (!Array.prototype.mapUsingReduce) {
Array.prototype.mapUsingReduce = function (callback, thisArg) {
return this.reduce(function (mappedArray, currentValue, index, array) {
mappedArray[index] = callback.call(thisArg, currentValue, index, array);
return mappedArray;
}, []);
};
}
cf) JavaScript reduce()
polyfill
// ECMA-262의 진행 단계, 5판(Edition), 15.4.4.21
// 참조: http://es5.github.io/#x15.4.4.21
// https://tc39.github.io/ecma262/#sec-array.prototype.reduce
if (!Array.prototype.reduce) {
Object.defineProperty(Array.prototype, "reduce", {
value: function (callback /*, initialValue*/) {
if (this === null) {
throw new TypeError(
"Array.prototype.reduce " + "called on null or undefined",
);
}
if (typeof callback !== "function") {
throw new TypeError(callback + " is not a function");
}
// 1. Let O be ? ToObject(this value).
var o = Object(this);
// 2. Let len be ? ToLength(? Get(O, "length")).
var len = o.length >>> 0;
// Steps 3, 4, 5, 6, 7
var k = 0;
var value;
if (arguments.length >= 2) {
value = arguments[1];
} else {
while (k < len && !(k in o)) {
k++;
}
// 3. If len is 0 and initialValue is not present,
// throw a TypeError exception.
if (k >= len) {
throw new TypeError(
"Reduce of empty array " + "with no initial value",
);
}
value = o[k++];
}
// 8. Repeat, while k < len
while (k < len) {
// a. Let Pk be ! ToString(k).
// b. Let kPresent be ? HasProperty(O, Pk).
// c. If kPresent is true, then
// i. Let kValue be ? Get(O, Pk).
// ii. Let accumulator be ? Call(
// callbackfn, undefined,
// « accumulator, kValue, k, O »).
if (k in o) {
value = callback(value, o[k], k, o);
}
// d. Increase k by 1.
k++;
}
// 9. Return accumulator.
return value;
},
});
}
map()
, filter()
, reduce()
로 반복문을 대체할 수 있다. 그래서 함수형 프로그래밍에서는 이 세 함수를 도구로 자주 활용한다는 점을 알 수 있었다.
map()
은 배열의 모든 원소에 함수를 적용할 때, filter()
는 어떤 배열의 하위 집합을 선택해 새로운 배열로 만들 때, reduce()
는 초깃값으로 배열의 원소를 조합해 하나의 값을 만들 때 사용한다.
다만, 이 책은 ES5 문법을 기준으로 설명하기 때문에 직접 세 함수를 구현하는 방법을 설명했지만, 지금은 이미 map()
, filter()
, reduce()
가 충분히 잘 구현되어있어서 mdn을 통해 polyfill로 구현 방법을 비교해보는 것도 의미 있었다고 생각한다.
세 함수는 프레임워크에서 컴포넌트를 가져와 UI에 뿌려줄 때도 자주 사용하기 때문에 프론트엔드 개발자라면 사용법을 잘 익혀둘 수밖에 없는 함수들인데 이렇게 구체적으로 다룰 기회가 있어서 좋았다😄