🔥 학습목표
- 일급객체에 대해 설명할 수 있다.
- 고차함수에 대해 설명할 수 있다.
- 배열 내장 고차함수 filter, map, reduce를 활용하여 프로그램을 작성할 수 있다.
- [S1U5] JS 함수, [S1U8] JS 배열, 자바스크립트 Deep Dive 18장을 복습하며 일급 객체 및 고차함수에 대해 더 깊게 공부한다.
다음 세 가지 조건을 가지면 일급 객체다.
- 변수에 할당(assignment)할 수 있다.
- 다른 함수의 전달인자(argument)로 전달될 수 있다.
- 다른 함수의 결과로서 리턴될 수 있다.
즉 객체를 일반 데이터(string
, number
, boolean
, array
, object
)처럼 다룰 수 있다는 것이다.
자바스크립트에서 함수는 일급 객체다.
함수가 일급 객체라는 것은, 함수를 객체와 동일하게 사용할 수 있다는 뜻이다. 객체는 값이므로 함수는 값과 동일하게 취급할 수 있다.
함수를 전달인자(argument)로 받을 수 있고, 함수를 리턴할 수 있는 함수
다른 함수(caller)의 전달인자로 전달되는 함수를 콜백 함수라고 한다.
주로 어떤 작업이 완료된 후 호출하는 경우가 많아 '콜백' 이라는 이름이 붙여졌다.
콜백 함수를 전달받은 고차함수(caller)는 함수 내부에서 콜백 함수를 호출할 수 있다.
혹은 조건에 따라 콜백 함수의 실행 여부를 결정할 수도 있다.
또는 아예 호출하지 않을 수도 있다.
아예 호출하지 않을 수도 있다.
여러 번 실행할 수도 있다.
(특정 작업이 끝난 뒤 호출하는 경우는 이후에 학습한다.)
커링 함수 / 고차 함수 뭐가 다를까?
- 커링 함수: '함수를 전달인자로 받는 함수' 라는 뜻에 한정.
- 정확하게 구분하자면, 고차 함수가 커링 함수를 포함한다.
모든 배열의 요소에서 특정 조건을 만족하는 요소를 걸러낸다.
└▷ 원본 배열은 변경되지 않는다.
let arr = [100, 200, 300, 400, 500];
const isOrderThan300 = function(num){
return num > 300;
}
let output = arr.filter(isOrderThan300);
console.log(output); // [400, 500]
=> 정리
배열의 각 요소가
특정 논리(함수)에 따라 true일 때
따로 분류한다(filter).
콜백 함수의 반환값들로 구성된 새로운 배열을 반환한다.
└▷ 원본 배열은 변경되지 않는다.
const albums = [
{
id: 1,
title: 'A Night Of The Opera',
year: 1975,
//... 이하 생략
},
{
id: 2,
title: 'Sheer Heart Attack',
//... 이하 생략
},
// ... 이하 생략
];
const findTitle = function(album){
return album.title;
}
const titles = albums.map(findTitle); // ['A Night Of The Opera', 'Sheer Heart Attack', ...]
=> 정리
배열의 각 요소가
특정 논리(함수)에 의해
다른 요소로 바뀌어 지정(map)된다.
배열의 모든 요소를 순회하며 인수로 전달 받은 콜백 함수를 반복 호출한 뒤, 하나의 결과값을 만들어 반환한다.
└▷ 원본 배열은 변경되지 않는다.
callback(초기값(이전 반환값), 요소값, 인덱스, 배열 자체)
const queen = [
{
name: 'Freddie Mercury',
age: 45,
//... 이하 생략
},
{
name: 'Brian May',
age: 76,
},
//...이하 생략
];
const ageReducer = function (sum, member){
return sum + member.age;
}
const memberAvgAge = queen.reduce(ageReducer, 0) / queen.length;
=> 정리
배열의 각 요소를
특정 방법(함수)에 따라
원하는 하나의 형태로
응축한다.(reduction)
function joinName(resultStr, member) {
return resultStr = resultStr + member.name + ',';
}
let members = [
{name: 'Freddie', age: 45},
{name: 'Brian', age: 76},
{name: 'Roger', age: 74},
{name: 'John', age: 72}
];
members.reduce(joinName, '');
function makeAdrressBook(addressBook, member){
let firstLetter = member.name[0];
if(firstLetter in addressBook)
addressBook[firstLetter].push(member);
else {
addressBook[firstLetter] = [];
addressBook[firstLetter].push(member);
}
return addressBook;
}
let members = [
{name: 'Freddie', age: 45},
{name: 'Brian', age: 76},
{name: 'Roger', age: 74},
{name: 'John', age: 72}
];
members.reduce(makeAddressBook, {});
const values = [100, 200, 300, 400, 500];
const max = values.reduce((acc, cur)=>(acc>cur? acc : cur), 0);
하지만 최대값을 구할 때는 Math.max
메서드를 쓰는 게 더 직관적이다.
const max = Math.max(...values);
const alphabets = ['a', 'a', 'b', 'c', 'd'];
const count = alphabets.reduce((acc, cur)=>{
// 빈 객체에 요소값인 cur을 프로퍼티 키로, 요소의 개수를 값으로 할당한다.
acc[cur] = (acc[cur] || 0) + 1;
return acc;
}, {})
// {'a':1} => {'a':2} => {'a':2, 'b':1} => {'a':2, 'b':1, 'c':1} => ...
const values = [1, [2,3], 4, [5,6]];
const flatten = values.reduce((acc,cur)=>acc.concat(cur), []);
하지만 중첩 배열을 평탄화 할 때는 Array.prototype.flat
메서드를 사용하는 게 더 직관적이다.
const alphabets = ['a', 'a', 'b', 'c', 'd'];
const result = alphabets.reduce(
// (초기값, 요소값, 인덱스, 배열 자체)
(unique, val, i, _values) =>
// 현재 순회 중인 요소의 인덱스가 val의 인덱스와 같다면 val은 처음 순회하는 요소다.
// 같지 않으면 val은 중복된 요소다.
// 처음 순회하는 요소만 초기값 빈 배열에 담는다.
_values.indexOf(val) === i ? [...unique, val] : unique,
[])
);
이건 코플릿 문제였는데 (수학 공식처럼) 아예 외우는 식으로 기억하고 있어도 나쁘지 않다고 하여 적어둔다.
function getIndex(arr, num) {
// num을 추가하고 정렬한다는 가정 하에, num의 위치 찾기
return arr.filter(function (el) {
return el < num; // num의 위치를 찾는 거지만 부등호를 넣지 않는 이유는
}).length; // length 를 반환할 것이기 때문이다. (+1 됨)
}
고차 함수를 쓰는 이유가 뭘까? 에 대하여 설명한다.
복잡한 어떤 것을 압축해서 추출한 상태로 만드는 것
프로그램을 작성할 때, 자주 반복해서 사용되는 로직은 별도의 함수로 작성한다. 이 역시 추상화의 좋은 예다.
함수를 통해 얻은 이러한 추상화를 한 단계 더 높인 것이 고차 함수다.
함수 = 값을 전달 받아 리턴 = 값에 대한 복잡한 로직은 감춰져 있다 = 값 수준의 추상화
값 수준의 추상화 = 단순히 값(value)을 전달받아 처리하는 수준
사고의 추상화 = 함수(사고의 묶음)를 전달받아 처리하는 수준
고차함수 = 함수를 전달 받거나 함수를 리턴 = 사고(함수)에 대한 복잡한 로직은 감춰져 있다 = 사고 수준에서의 추상화
=> 추상화의 수준이 높아지면 생산성도 상승한다.
아래와 같은 데이터가 있다고 할 때,
const data = [
{
gender: 'male',
age: 24,
},
{
gender: 'male',
age: 25,
},
{
gender: 'female',
age: 27,
},
{
gender: 'female',
age: 22,
}
];
남성들의 평균 나이를 구하는 하나의 함수를 작성하면 아래와 같다.
function getAverageAgeOfMaleAtOnce(data) {
const onlyMales = data.filter(function (d) {
return d.gender === 'male';
});
const numOfMales = onlyMales.length;
const onlyMaleAges = onlyMales.map(function (d) {
return d.age;
});
const sumOfAges = onlyMaleAges.reduce(function (acc, cur) {
return acc + cur;
}, 0);
return sumOfAges / numOfMales;
}
하지만 위 작업은 '남성'들의 평균 나이만 구할 수 있다.
저 함수를 조금 더 사용성 있게 바꾸려면 '성별'이라는 파라미터를 만드는 게 좋다.
그럼에도 저 함수 속에는 '평균 나이를 구하는' 데에만 쓰이긴 아까운 부분들이 있다.
map
을 사용하여 age
만 분리하거나, reduce
를 사용하여 평균을 내는 함수는
'남성' 중 '최연소 나이'를 구하거나, '여성' 중 '최연소 나이와 최연장 나이의 차이' 등 충분히 다른 목적을 위해 재사용 될 수 있다.
따라서 아래와 같이 재사용 될만한 가치가 있는 것들의 단위로 함수를 분리할 수 있다.
function getOnlyFemales(data) {
return data.filter(function(d){
return d.gender === 'female';
});
}
function getOnlyAges(data) {
return data.map(function(d){
return d.age;
});
}
function getAverage(data){ // 생략 }
// pipe 함수
function compose(...funcArgs) {
return function(data){
let result = data;
for(let i = 0; i<funcArgs.length; i++)
result = funcArgs[i](result);
return result;
};
}
const getAverageAgeOfFemale = compose(
getOnlyFemales,
getOnlyAges,
getAverage
};
const result = getAverageAgeOfFemale(data);