코드스테이츠 - 유어클래스 콘텐츠를 참고하여 작성하였습니다.

[Day6]

2023년 7월 3일

📗목차

일급 객체

JavaScript에는 특별한 대우를 받는 일급 객체(first-class citizen)가 있다.
대표적인 일급 객체 중 하나가 함수인데, 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

고차 함수

고차 함수(higher order function)는 함수를 전달인자(argument)로 받을 수 있고, 함수를 리턴할 수 있는 함수이다.

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

3.1 응용 버전

  • 문제
    • 함수를 리턴해야 합니다.
    • 리턴되는 함수는 정수를 입력받아 func1, func2, ..., funcN의 순으로 적용합니다.

▼ 입출력 예시

function square(num) { return num * num; }
function add5(num) { return num + 5; }
function mul3(num) { return num * 3; }
function isOdd(num) { return num % 2 !== 0; }

let output = pipe(add5, square);
console.log(output(4)); // --> 81

output = pipe(square, add5, mul3);
console.log(output(4)); // --> 63

output = pipe(square, mul3, add5, add5, isOdd);
console.log(output(4)); // --> false
  • 문제 풀이
function pipe(...func) {
  return function(num) {
    let res = num;
    for(let i = 0; i < func.length; i++)
      res = func[i](res);
    return res;
  }
}

내장 고차 함수

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

배열 메서드

  • filter

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

// 함수 표현식
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']
// 단행본 모음
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

하나의 데이터를 다른 데이터로 매핑(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); // ['어머니의 쌀', ...]
const solution = (numbers) => numbers.map((number) => number * 2) // [2, 4, 6, 8]
solution([1, 2, 3, 4]);
  • reduce

여러 데이터를, 하나의 데이터로 응축(reduce)할 때 사용

<Execution - 초기값을 정하지 않았을 때>

<Execution - 초기값을 정했을 때>

// 위의 사진에 잘못된 코드 정정
const arr = [1, 2, 3];
const result = arr.reduce((acc, cur, idx) => {
  return acc + cur;
}, 1)
result;
  1. acc = acc(1) + cur(1) = 2
  2. acc = acc(2) + cur(2) = 4
  3. acc = acc(4) + cur(3) = 7
  4. result = 7

// 단행본 모음
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 메서드로 모든 책의 누적 평균을 구할 수 있다.

function joinName(resultStr, user) {
  resultStr = resultStr + user.name + ', ';
  return resultStr;
}

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

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

// 따로 함수를 정의하지 않고, reduce 메서드에서 바로 함수를 정의해도 됨. 
let res = users.reduce((result, user) => {
  result += user.name + ', ';
  return result;
}, '');

[코드] 콜백 함수 joinName은 users 배열 안에 있는 요소의 이름을 하나로 응축

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 }
];

let res = users.reduce(makeAddressBook, {});

[코드] 콜백 함수 makeAddressBook은 users 배열 안에 있는 요소로 주소록을 만듦.

// 최종 리턴 값
{
  T: [
    { name: 'Tim', age: 40 }
  ],
  S: [
    { name: 'Satya', age: 30 },
    { name: 'Sundar', age: 50 }
  ]
}

고차 함수를 쓰는 이유

  • 추상화 = 생산성(productivity)의 향상

한편 프로그램을 작성할 때, 자주 반복해서 사용하는 로직은 별도의 함수로 작성하기도 한다. 이 역시 추상화의 좋은 사례이고, 추상화의 관점에서 함수를 바라보면, 함수는 사고(thought) 또는 논리(logic)의 묶음이다.

아래의 getAverage 함수는 number 타입을 요소로 갖는 배열을 입력받아, 모든 요소의 평균값을 리턴한다. 인자를 전달하기만 하면, 복잡한 로직은 신경 쓰지 않아도 평균값을 얻을 수 있다.

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 함수는 값(배열)을 전달받아, 이 값을 가지고 복잡한 작업을 수행하고 이는 값 수준에서의 추상화이다.

함수
= 값을 전달받아 값을 리턴한다.
= 값에 대한 복잡한 로직은 감추어져 있다.
= 값 수준에서의 추상화
고차 함수는 이 추상화의 수준을 사고의 추상화 수준으로 끌어올린다.

값 수준의 추상화: 단순히 값(value)을 전달받아 처리하는 수준
사고의 추상화: 함수(사고의 묶음)를 전달받아 처리하는 수준
다시 말해 고차 함수를 통해, 보다 높은 수준(higher order)에서 생각할 수 있다.

고차 함수
= 함수를 전달받거나 함수를 리턴한다.
= 사고(함수)에 대한 복잡한 로직은 감추어져 있다.
= 사고 수준에서의 추상화

profile
프론트엔드 개발자입니다.

0개의 댓글