[JS]고차 함수

nada_1221·2022년 7월 21일
0

공부

목록 보기
13/49

일급 객체


JS에도 특별한 대우를 받는 일급 객체 first-class citizen이 있다. 대표적인 일급 객체 중 하나가 함수이다.
JS에서 함수는 아래와 같이 특별 취급하게 된다.

  • 변수에 할당assignment 할 수 있다.
  • 다른 함수의 전달인자argument로 전달될 수 있다.
  • 다른 함수의 결과로서 리턴될 수 있다.

함수를 변수에 할당할 수 있기 때문에, 함수를 배열의 요소나 객체의 속성 값으로 저장할 수 있다. 함수를 데이터 (string,number,boolean,array,object) 처럼 다룰 수 있다.

  1. 변수에 함수를 할당하는 경우
/*
 * 아래는 변수 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

함수는 변수에 저장된 데이터를 전달 인자로 받거나, 리턴 값으로 사용할 수 있습니다. 함수도 변수에 저장될 수 있기 때문에 함수를 인자로 받거나, 리턴 값으로 사용할 수 있습니다.

고차 함수의 이해

--- 고차 함수higher order function는 함수를 전달인자argument로 받을 수 있고, 함수를 리턴할 수 있는 함수이다.
함수는 변수에 저장할 수 있다. 그리고 함수는, 함수를 담은 변수를 전달인자로 받을 수 있다. 마찬가지로, 함수 내부에서 변수에 함수를 할당할 수 있다 함수는 이 변수를 리턴할 수 있다. 여기서 변수에 할당하지 않고 함수를 바로 이용할 수 있다. 어떤 고차 함수에 함수를 전달인자로 전달하고, 고차 함수는 함수 자체를 리턴한다. 변수가 빠졌을 뿐, 동일하게 동작한다.

이때 다른 함수caller의 전달인자argument로 전달되는 함수를 콜백 함수callback function이라고 한다. 어떤 작업이 완료되었을 때 호출하는 경우가 많아서, 답신 전화를 뜻하는 콜백 함수라는 이름이 붙여졌다.

콜백 함수를 전달받은 고차 함수caller는, 함수 내부에서 이 콜백 함수를 호출invoke할 수 있고, 조건에 따라 콜백 함수의 실행 여부를 결정할 수도 있다. 아예 호출하지 않을 수도 있고, 여러번 실행할 수도 있다. 특정 작업의 완료 후에 호출하는 경우는 이후에 충분히 접할 수 있다.

'함수를 리턴하는 함수'는 모양새가 특이한 만큼, 부르는 용어가 따로 있다. '함수를 리턴하는 함수'를 고안해 낸 논리학자 하스켈 커리Haskell Curry의 이름을 따, 커링 함수라고 한다. 따로 커링 함수라는 용어를 사용하는 경우에는, 고차 함수라는 용어를 '함수를 전달인자로 받는 함수'에만 한정해 사용하기도 한다. 그러나 정확하게 구분하자면, 고차 함수가 커링 함수를 포함한다.

  1. 다른 함수를 인자로 받는 경우
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

2.함수를 리턴하는 경우

function adder(added) {
  return function (num) {
    return num + added;
  };
}

/*
 * 함수 adder는 다른 함수를 리턴하는 고차 함수입니다.
 * 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

3.함수를 인자로 받고, 함수를 리턴하는 경우

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

내장 고차 함수 이해하기


JS에는 기본적으로 내장된 고차 함수가 여럿 있다. 그중에서 배열 메서드들 중 일부가 대표적인 고차 함수에 해당한다.

배열의 filter 메서드는 모든 요소 중에서 특정 조건을 만족하는 요소를 걸러내는 매서드이다.

// 아래 코드에서 '짝수'와 '길이 5 이하'는 문법 오류(syntax error)에 해당합니다.
// 의미만 이해해도 충분합니다.
let arr = [1, 2, 3, 4];
let output = arr.filter(짝수);
console.log(output); // ->> [2, 4]

arr = ['hello', 'code', 'states', 'happy', 'hacking'];
output = arr.filter(길이 5 이하)
console.log(output); // ->> ['hello', 'code', 'happy']

여기서 걸래내는 기준이 되는 특정 조건은 filter 메서드의 전달인자로 전달됩니다. 이때 전달되는 조건은 함수의 형태입니다.
filter 메서드는, 걸러내기 위한 조건을 명시한 함수를 전달인자로 받기 때문에 고차 함수이다.

// 아래 코드는 정확한 표현 방식은 아닙니다.
// 의미만 이해해도 충분합니다.

let arr = [1, 2, 3];
// 배열의 filter 메서드는 함수를 전달인자로 받는 고차 함수입니다.
// arr.filter를 실행하면 내부적으로 arr에 접근할 수 있다고 생각해도 됩니다.
arr.filter = function (arr, func) {
  const newArr = [];
  for (let i = 0; i < arr.length; i++) {
    // filter에 전달인자로 전달된 콜백 함수는 arr의 각 요소를 전달받아 호출됩니다.
    // 콜백 함수가 true를 리턴하는 경우에만 새로운 배열에 추가됩니다.
    if (func(arr[i]) === true) {
      newArr.push(this[i]);
    }
  }
  // 콜백 함수의 결과가 true인 요소들만 저장된 배열을 리턴합니다.
  return newArr;
};

/*
 * filter 메서드의 보다 정확한 정의는 아래와 같습니다. 아래 코드를 이해하기 위해서는 다음 유닛에서 프로토타입과 this에 대한 학습이 필요합니다.
 * Array.prototype.filter = function(func) {
 *   const arr = this;
 *   const newArr = []
 *   for(let i = 0; i < arr.length; i++) {
 *     if (func(arr[i]) === true) {
 *       newArr.push(this[i])
 *     }
 *   }
 *   return newArr;
 * }
 */

filter 메서드는 배열의 요소를 콜백 함수에 다시 전달합니다. 콜백 함수는 전달받은 배열의 요소를 받아 함수를 실행하고, 콜백 함수 내부의 조건에 따라 참(true) 또는 거짓(false)을 리턴해야 합니다. 처음 본 코드에 이 점을 반영하여 다시 코드를 작성하면, 다음과 같습니다.

// 함수 표현식
const isEven = function (num) {
  return num % 2 === 0;
};

let arr = [1, 2, 3, 4];
// let output = arr.filter(짝수);
// '짝수'를 판별하는 함수가 조건으로서 filter 메서드의 전달인자로 전달됩니다.
let output = arr.filter(isEven);
console.log(output); // ->> [2, 4]

const isLteFive = function (str) {
  // Lte = less then equal
  return str.length <= 5;
};

arr = ['hello', 'code', 'states', 'happy', 'hacking'];
// output = arr.filter(길이 5 이하)
// '길이 5 이하'를 판별하는 함수가 조건으로서 filter 메서드의 전달인자로 전달됩니다.
let output = arr.filter(isLteFive);
console.log(output); // ->> ['hello', 'code', 'happy']

실제 filter 활용 예시


filter 활용 시, 아래 과정을 꼭 기억하세요.

  • 배열의 각 요소가
  • 특정 논리(함수)에 따르면, 사실(true)일 때
  • 따로 분류합니다(filter).

문제

만화책 식객 27권의 정보가 배열에 담겨있습니다. 출판 연도가 2003년인 단행본만 담은 배열을 만드세요.

수도코드

  • 배열의 각 요소 : 각 식객 1- 27권의 정보
  • 특정 논리(함수) : 책의 출판 연도가 2003년입니다. (true / false)
  • 따로 분류 : 출판 연도가 2003년인 책의 정보

실제 코드

filter는 이렇게 조건에 맞는 데이터만 분류(filtering) 할 때 사용합니다.

// 단행본 모음
const cartoons = [
  {
    id: 1,
    bookType: 'cartoon',
    title: '식객',
    subtitle: '어머니의 쌀',
    createdAt: '2003-09-09',
    genre: '요리',
    artist: '허영만',
    averageScore: 9.66,
  },
  {
    id: 2,
    // .. 이하 생략
  },
  // ... 이하 생략
]; 

// 단행본 한 권의 출판 연도가 2003인지 확인하는 함수
const isCreatedAt2003 = function (cartoon) {
  const fullYear = new Date(cartoon.createdAt).getFullYear()
  return fullYear === 2003;
}; 

// 출판 연도가 2003년인 책의 모음
const filteredCartoons = cartoons.filter(isCreatedAt2003); 

실제 map 활용 예시


map 활용 시, 아래 과정을 꼭 기억하세요.

  • 배열의 각 요소가
  • 특정 논리(함수)에 의해
  • 다른 요소로 지정(map) 됩니다.

문제

만화책 식객 27권의 정보가 배열에 담겨있습니다. 각 책의 부제(subtitle)만 담은 배열을 만드세요.

수도 코드

  • 배열의 각 요소 : 각 식객 1- 27권의 정보
  • 특정 논리(함수) : 책 한 권의 부제를 찾습니다.
  • 다른 요소로 지정 : 각 식객 1- 27권의 부제

실제 코드

map은 이렇게 하나의 데이터를 다른 데이터로 매핑(mapping) 할 때 사용합니다.

// 만화책 모음
const cartoons = [
  {
    id: 1,
    bookType: 'cartoon',
    title: '식객',
    subtitle: '어머니의 쌀',
    createdAt: '2003-09-09',
    genre: '요리',
    artist: '허영만',
    averageScore: 9.66,
  },
  {
    id: 2,
    // .. 이하 생략
  },
  // ... 이하 생략
]; 

// 만화책 한 권의 부제를 리턴하는 로직(함수)
const findSubtitle = function (cartoon) {
  return cartoon.subtitle;
}; 

// 각 책의 부제 모음 
const subtitles = cartoons.map(findSubtitle); // ['어머니의 쌀', ...]

실제 reduce 활용 예시


reduce 활용 시, 아래 과정을 꼭 기억하세요.

  • 배열의 각 요소를
  • 특정 방법(함수)에 따라
  • 원하는 하나의 형태로
  • 응축합니다. (reduction)

문제

만화책 식객 27권의 정보가 배열에 담겨있습니다. 각 단행본의 평점의 평균을 리턴하세요.

수도코드

  • 배열의 각 요소 : 각 식객 1- 27권의 정보
  • 응축하는 방법 (함수) : 각 단행본의 평점을 누적값에 더합니다.
  • 원하는 형태 : 숫자로 누적합니다.
  • 응축된 결과 : 각 단행본의 평점의 합을 단행본의 길이로 나눈 평점의 평균

실제 코드

reduce는 이렇게 여러 데이터를, 하나의 데이터로 응축(reduce)할 때 사용합니다.

// 단행본 모음
const cartoons = [
  {
    id: 1,
    bookType: 'cartoon',
    title: '식객',
    subtitle: '어머니의 쌀',
    createdAt: '2003-09-09',
    genre: '요리',
    artist: '허영만',
    averageScore: 9.66,
  },
  {
    id: 2,
    // .. 이하 생략
  },
  // ... 이하 생략
];

// 단행본 한 권의 평점을 누적값에 더한다.
const scoreReducer = function (sum, cartoon) {
  return sum + cartoon.averageScore;
}; 

// 초기값에 0을 주고, 숫자의 형태로 평점을 누적한다.
let initialValue = 0 
// 모든 책의 평점을 누적한 평균을 구한다.
const cartoonsAvgScore = cartoons.reduce(scoreReducer, initialValue) / cartoons.length;

reduce의 색다른 사용법


배열을 문자열로

수도 코드

  • 배열의 각 요소 : 유저 정보
  • 응축하는 방법 (함수) : 하나의 유저의 이름과 쉼표를 이어 붙입니다(concat)
  • 원하는 형태 : 문자열로 누적합니다.
  • 응축된 결과 : 쉼표로 구분되는 모든 유저의 이름
function joinName(resultStr, user) {
  resultStr = resultStr + user.name + ', ';
  return resultStr;
}

let users = [
  { name: 'Tim', age: 40 },
  { name: 'Satya', age: 30 },
  { name: 'Sundar', age: 50 }
];

users.reduce(joinName, '');
//console.log(users.reduce(joinName,''));
//= Tim, Satya, Sundar

실습


reduce의 콜백 함수 joinName은 총 몇 번 실행되나요? 다음 표에 들어가는 값은 각각 무엇인가요?

호출 횟수  | resultStr | user | 리턴 값
-------- | --------- | -------- | ---------
1번째 호출 |  `	   ` | ` Tim  ` | ` Tim, `
2번째 호출 |  ` TIm, ` | `Satya`  | `Tim,Satya`
n번째 호출 |`Tim,Satya`| `Sundar` | `Tim,Satya,Sundar`
~~~
 
최종 리턴 값 : Tim, Satya, Sundar

배열을 객체로


수도 코드

  • 배열의 각 요소 : 유저 정보
  • 응축하는 방법 (함수) : 유저 한 명의 이름 중 첫 글자를 주소록 객체 속성의 키(key)로, 유저의 정보를 주소록 객체 속성의 값(value)으로 추가합니다.
  • 원하는 형태 : 주소록 객체에 누적합니다.
  • 응축된 결과 : 모든 유저의 정보가 알파벳으로 구분된 주소록
function makeAddressBook(addressBook, user) {
  let firstLetter = user.name[0];

  if(firstLetter in addressBook) {
    addressBook[firstLetter].push(user);
  } else {
    addressBook[firstLetter] = [];
    addressBook[firstLetter].push(user);
  }

  return addressBook;
}

let users = [
  { name: 'Tim', age: 40 },
  { name: 'Satya', age: 30 },
  { name: 'Sundar', age: 50 }
];

users.reduce(makeAddressBook, {});
console.log(users.reduce(makeAddressBook,{}));

실습


reduce의 콜백 함수 makeAddressBook은 총 몇 번 실행되나요? 다음 표에 들어가는 값은 각각 무엇인가요?

호출 횟수 | addressBook |   user   | 리턴 값
-------- | ---------- | -------- | ---------
1번째 호출 |  `       ` | `Tim`    | `T:{arry}`
2번째 호출 |  `T`       | `Satya`  | `T:{},S:{}`
n번째 호출 |  `TS`      | `Sundar` | `T:{},S:{S+@}`

//최종 리턴 값

{
  T: [
    { name: 'Tim', age: 40 }
  ],
  S: [
    { name: 'Satya', age: 30 },
    { name: 'Sundar', age: 50 }
  ]
}
profile
FE_개발자_지망생

0개의 댓글