일급 객체
다른 객체들에 일반적으로 적용 가능한 연산을 모두 지원하는 객체를 가리킨다.
함수는 JavaScript의 대표적인 일급 객체 중 하나로, 아래와 같은 특징을 갖는다.
함수를 변수에 할당할 수 있기 때문에, 함수를 배열의 요소나 객체의 속성값으로 저장할 수 있다. 즉, 함수를 데이터(string, number, array, object)처럼 다룰 수 있다.
/*
* 아래는 변수 square에 함수를 할당하는 함수 표현식입니다.
* JavaScript에서 함수는 일급 객체이기 때문에 변수에 할당할 수 있습니다.
*
* 함수 표현식은 할당 전에 사용할 수 없습니다.
* square(7); // --> ReferenceError: Can't find variable: square
*/
const square = function (num) {
return num * num;
};
// 변수 square에는 함수가 할당되어 있으므로 (일급 객체), 함수 호출 연산자 '()'를 사용할 수 있습니다.
output = square(7);
console.log(output); // --> 49
함수를 전달인자로 받을 수 있고, 함수를 리턴할 수 있는 함수를 의미한다.
함수는 일급 객체이기 때문에 고차함수로 사용할 수 있다.
이 때, 다른 함수(고차함수: caller)의 전달인자로 전달되는 함수를 콜백 함수(callback function)라고 한다.
function double(num) {
return num * 2;
}
function doubleNum(func, num) {
return func(num);
}
/*
* 함수 doubleNum은 다른 함수를 인자로 받는 고차 함수입니다.
* 함수 doubleNum의 첫 번째 인자 func에 함수가 들어올 경우
* 함수 func는 함수 doubleNum의 콜백 함수입니다.
* 아래와 같은 경우, 함수 double은 함수 doubleNum의 콜백 함수입니다.
*/
let output = doubleNum(double, 4);
console.log(output); // -> 8
함수를 return하는 함수는 고안해 낸 논리학자 하스켈 커리의 이름을 따, '커링 함수'라고 한다.
해당 용어를 따로 사용하는 경우는 고차함수라는 용어를 '함수를 전달인자로 받는 함수'로 한정해 사용하기도 하지만 정확하게는 고차함수가 커링함수를 포함하는 것이다.
function adder(added) {
return function (num) {
return num + added;
};
}
/*
* 함수 adder는 다른 함수를 리턴하는 고차 함수입니다.(caller)
* adder는 인자 한 개를 입력받아서 함수(익명 함수)를 리턴합니다.
* 리턴되는 익명 함수는 인자 한 개를 받아서 added와 더한 값을 리턴합니다.
*/
// adder(5)는 함수이므로 함수 호출 연산자 '()'를 사용할 수 있습니다.
let output = adder(5)(3); // -> 8
console.log(output); // -> 8
// adder가 리턴하는 함수를 변수에 저장할 수 있습니다.
// javascript에서 함수는 일급 객체이기 때문입니다.
const add3 = adder(3);
output = add3(2);
console.log(output); // -> 5
function double(num) {
return num * 2;
}
function doubleAdder(added, func) {
const doubled = func(added);
return function (num) {
return num + doubled;
};
}
/*
* 함수 doubleAdder는 고차 함수입니다.
* 함수 doubleAdder의 인자 func는 함수 doubleAdder의 콜백 함수입니다.
* 함수 double은 함수 doubleAdder의 콜백으로 전달되었습니다.
*/
// doubleAdder(5, double)는 함수이므로 함수 호출 기호 '()'를 사용할 수 있습니다.
doubleAdder(5, double)(3); // -> 13
// doubleAdder가 리턴하는 함수를 변수에 저장할 수 있습니다. (일급 객체)
const addTwice3 = doubleAdder(3, double);
addTwice3(2); // --> 8
높은 수준의 추상화를 통해 생산성을 향상할 수 있기 때문이다.
⭐ 추상화?
복잡한 어떤 것을 압축해서 핵심만 추출한 상태로 만드는 것
아래의 코드를 보면, getAverage 함수가 배열을 인자로 받아 평균값을 return하는 것을 알 수 있다.
function getAverage(data) {
let sum = 0;
for (let i = 0; i < data.length; i++) {
sum = sum + data[i];
}
return sum / data.length;
}
let output = getAverage([1, 2, 3]);
console.log(output); // --> 2
output = getAverage([4, 2, 3, 6, 5, 4]);
console.log(output); // --> 4
이 때, getAverage 함수는 값(배열)을 전달받아, 이 값을 가지고 복잡한 작업을 수행한다. => '값 수준의 추상화'
고차함수는 이 추상화의 수준을 사고의 추상화 수준으로 끌어올리는데, 위에 예시와 같은 함수를 전달받아 처리하는 수준이다.
고차 함수 = 함수를 전달받거나 return 한다 = 사고(함수)에 대한 복잡한 로직은 감추어져 있다 = 사고 수준에서의 추상화
추상화의 수준이 높아지는 만큼 생산성은 비약적으로 상승한다.
const data = [
{gender: 'male',age: 24,},
{gender: 'male',age: 25,},
{gender: 'female',age: 27,},
{gender: 'female',age: 22,},
{gender: 'male', age: 29,},
];
위와 같은 데이터를 순차적으로 처리할 때, 모든 작업을 함수로 작성할 수 있다. ex) 남성들의 평균 나이을 구한다.
function getAverageAgeOfMaleAtOnce(data) {
const onlyMales = data.filter(function (d) {
// data.filter는 배열의 각 요소에 인자로 전달받은 함수를 적용하고,
// 그 결과가 true인 요소만을 갖는 배열을 리턴합니다.
return d.gender === 'male';
});
const numOfMales = onlyMales.length;
const onlyMaleAges = onlyMales.map(function (d) {
// onlyMales.map는 배열의 각 요소에 인자로 전달받은 함수를 적용하고,
// 각 결과를 요소로 갖는 배열을 리턴합니다.
return d.age;
});
const sumOfAges = onlyMaleAges.reduce(function (acc, cur) {
// onlyMaleAges.reduce는 배열의 각 요소에 인자로 전달받은 함수를 적용하고,
// 각 결과를 두 번째 인자로 전달받은 초기값(0)에 누적한 결과를 리턴합니다.
return acc + cur;
}, 0);
return sumOfAges / numOfMales;
}
console.log(getAverageAgeOfMaleAtOnce(data)); // 26
위의 getAverageAgeOfMaleAtOnce
함수는 남성의 평균 나이를 구하는 작업에서 사용할 수 있다.
그러나, male
을 매개변수화하여 더 일반적인 함수로 변경가능하며 수정 후, 남성 및 여성의 평균나이를 구하는 작업을 수행할 수 있다.
한편, filter, map, reduce 등의 배열 메서드는 다른 목적을 위해서 사용될 수도 있다.(남성 중 최연소, 여성 중 최연소 등) 따라서, 이미 작성된 로직을 재사용할 수 있다.
추상화는 고차 함수를 통해, 보다 쉽게 달성할 수 있으며 아래의 compose 함수는 입력받은 함수를 순서대로 결합하는 고차 함수이다.
각각의 작업(filter, map, reduce)은 별도의 함수로 분리되어 compose의 인자로 전달되는 callback 함수가 된다.
function getOnlyMales(data) {
return data.filter(function (d) {
return d.gender === 'male';
});
}
function getOnlyAges(data) {
return data.map(function (d) {return d.age;
});
}
function getAverage(data){
const sum = data.reduce(function(acc, cur){
return acc + cur;
},0);
return sum / data.length
}
function compose(...funcArgs){
// compose는 여러 개의 함수를 인자로 전달받아 함수를 리턴하는 고차 함수입니다.
// compose가 리턴하는 함수(익명 함수)는 임의의 타입의 data를 입력받아,
return function(data){
// funcArgs의 요소인 함수들을 차례대로 적용(apply)시킨 결과를 리턴합니다.
let result = data;
for(let i = 0; i < funcArgs.length; i++){
result = funcArgs[i](result);
}
return result;
};
}
// compose를 통해 함수들이 순서대로 적용된다는 것이 직관적으로 드러납니다.
// 각각의 함수는 다른 목적을 위해 재사용(reuse) 될 수 있습니다.
const getAverageAgeOfMale = compose(
getOnlyMales, // 배열을 입력받아 배열을 리턴하는 함수
getOnlyAges, // 배열을 입력받아 배열을 리턴하는 함수
getAverage // 배열을 입력받아 `number` 타입을 리턴하는 함수
);
const result = getAverageAgeOfMale(data);
console.log(result); // --> 26
바로 위의 예시처럼 고차 함수를 통해 사고 수준에서의 추상화를 달성할 수 있고, 각각의 작업은 다른 목적으로 재사용될 수 있으므로 생산성을 높일 수 있다.
Reference:
코드스테이츠
MDN: https://developer.mozilla.org/ko/docs/Glossary/First-class_Function