Unit1 - [JavaScript] 고차 함수

강성일·2023년 5월 10일
0
post-thumbnail

✅ TIL



일급 객체

일급 객체(First-class object)란 프로그래밍 언어에서 값으로 다뤄질 수 있는 개체를 말한다.
즉, 변수에 할당하거나 함수의 인자로 전달하고, 함수의 반환 값으로 사용할 수 있는 객체를 의미한다.

대표적인 일급 객체 중 하나가 함수다.

JavaScript에서 함수는 아래와 같이 특별하게 취급한다.

  • 변수에 할당(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
[코드] 함수 표현식은 변수에 할당한 다음 사용할 수 있다.

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

함수도 변수에 저장될 수 있기 때문에 함수를 인자로 받거나, 리턴 값으로 사용할 수 있다.



고차 함수

고차 함수는 함수를 다루는 데 특화된 개념으로, 함수를 인자로 받거나 함수를 반환하는 함수를 말한다.
즉, 함수를 변수에 할당하거나 데이터처럼 다룰 수 있는 함수다.


이전 콘텐츠에서 확인했듯이, 함수는 변수에 저장할 수 있다.
그리고 함수는, 함수를 담은 변수를 전달인자로 받을 수 있다.
마찬가지로, 함수 내부에서 변수에 함수를 할당할 수 있다. 그리고 함수는 이 변수를 리턴할 수 있다.

여기서 변수에 할당하지 않고 함수를 바로 이용할 수 있다.
어떤 고차 함수에 함수를 전달인자로 전달하고, 고차 함수는 함수 자체를 리턴한다.
변수가 빠졌을 뿐, 동일하게 동작한다.

이때 다른 함수(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

  1. 함수를 리턴하는 경우
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

  1. 함수를 인자로 받고, 함수를 리턴하는 경우
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


내장 고차 함수


JavaScript에는 기본적으로 내장된 고차 함수가 여럿 있다.

그중에서 배열 메서드들 중 일부가 대표적인 고차 함수에 해당한다.



⚙️ filter

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

예를 들어 number 타입을 요소로 갖는 배열에서 짝수만을 걸러내거나, 18 보다 작은 수만을 걸러낸다.

string 타입을 요소로 갖는 배열에서,
길이가 10 이하인 문자열만 걸러내거나, 'korea' 같은 특정 문자열만 걸러낼 수도 있다.


// 아래 코드에서 '짝수'와 '길이 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 메서드는, 걸러내기 위한 조건을 명시한 함수를 전달인자로 받기 때문에 고차 함수이다.

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']

let output = arr.filter(isEven); 은 다음과 같다.

  • 💡 let output = arr.filter(function(num){return num % 2 === 0 });
  • 💡 let output = arr.filter(num => num % 2 === 0);

filter 함수는 고차 함수이며, isEven 함수는 일반 함수이며, 동시에 일급 객체로 다룰 수 있는 함수이다.


이유는 다음과 같다.

filter 함수는 배열의 각 요소를 검사하기 위해, 인자로 전달된 함수를 실행하며, 이를 통해 요소를 필터링한다.
반면에 isEven 함수는 다른 함수를 인자로 받거나 반환하지 않기 때문에 고차 함수는 아니다.

이제 고차함수를 왜 사용하는지 알 것 같고, 고차함수와 일급 객체의 개념이 좀 잡히는 느낌이 난다.



⚙️ map

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

Ex) 만화책 식객 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

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

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

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

reduce를 이용한 간단한 배열의 합

const arr = [1, 2, 3];
const result = arr.reduce((acc, cur, idx) => {
  let newAcc = acc + cur;
  return newAcc;
}, 1)
result;

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;


배열을 문자열로

  • 배열의 각 요소 : 유저 정보
  • 응축하는 방법 (함수) : 하나의 유저의 이름과 쉼표를 이어 붙입니다.(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, '');

배열을 객체로

  • 배열의 각 요소 : 유저 정보
  • 응축하는 방법 (함수) : 유저 한 명의 이름 중 첫 글자를 주소록 객체 속성의 키(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, {});


💡 filter와 reduce가 헷갈린다면 ?


filter: 주어진 조건에 맞는 요소들로 이루어진 새로운 배열을 생성한다.

  • filter 메서드는 콜백 함수를 사용하여 각 요소를 평가하고, 콜백 함수가 true를 반환하는 요소들만을 새로운 배열로 추출한다.
  • 반환된 배열의 길이는 원본 배열보다 작거나 같을 수 있다.
  • filter 메서드는 원본 배열을 변형하지 않고, 조건에 맞는 요소들을 추출하여 새로운 배열을 반환한다.


reduce: 배열의 모든 요소를 하나의 값으로 축소하는 작업을 수행한다.

  • reduce 메서드는 콜백 함수와 초기값을 인자로 받는다.
  • 콜백 함수는 누산기(accumulator)와 현재 요소를 인자로 받으며, 누산기에 요소를 반영하여 최종 결과를 반환한다.
  • 반환된 값은 최종적인 축소된 결과이다.
  • reduce 메서드는 원본 배열을 변형하지 않고, 최종 결과를 반환한다.

따라서, filter는 주어진 조건에 따라 요소를 추출하는데 사용되며, 새로운 배열을 반환한다.
반면에 reduce는 배열의 요소들을 하나의 값으로 축소하여 최종 결과를 반환한다.


아래 예제를 통해 filter와 reduce의 차이를 살펴보자.

const numbers = [1, 2, 3, 4, 5];

// filter 사용 예제: 짝수만 추출
const evenNumbers = numbers.filter(num => num % 2 === 0);
console.log(evenNumbers); // [2, 4]

// reduce 사용 예제: 모든 요소의 합 계산
const sum = numbers.reduce((accumulator, num) => accumulator + num, 0);
console.log(sum); // 15

filter 메서드를 사용하여 numbers 배열에서 짝수만을 추출했다.
결과적으로 [2, 4]라는 새로운 배열이 반환된다.

reduce 메서드를 사용하여 numbers 배열의 모든 요소를 더하여 합을 계산했다.
자세히 말하면, 초기값으로 0을 설정하고, 콜백 함수를 통해 누산기에 요소를 더하여 최종 합을 계산했다.
결과적으로 15라는 number 타입이 반환된다.

profile
아이디어가 넘치는 프론트엔드를 꿈꿉니다 🔥

0개의 댓글