[javascript] Array.filter() / map() / reduce() (feat. 고차함수)

Hyebin·2021년 4월 1일
0

Javascript

목록 보기
14/27
post-thumbnail

일급객체(First-class citizen)

first-class citizen이란 자유롭게 거주하고 일 할 수 있고, 출입국의 자유를 가지며, 투표의 자유를 가지는 시민을 의미한다.

자바스크립트에서도 특별한 대우를 받는 것들이 있는데 이런 것들을 일급 객체(first-class citizen)라고 한다.
일급 객체는 일반적으로 다른 객체들에게 적용 가능한 연산을 모두 지원하는 객체를 뜻한다.

first-class citizen(일급객체)의 조건은 아래와 같다.

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

일급객체 중 하나인 함수(function)

함수는 일급객체에 속한다. 이 말은 즉, 함수를 데이터(string, number, boolean, array, object)를 다루듯이 다룰 수 있다는 걸 의미한다. 변수에 저장할 수 있기 때문에 배열의 요소나 객체의 속성값이 될 수 있다.

몇가지 예시를 보며, 함수가 일급객체인지를 확인해보자.
1. 변수에 할당할 수 있다.
객체의 키에 값으로 함수를 할당해줄 수 있다. show라는 키에 함수를 할당해줬다.
music.show();로 함수가 실행된다.

let music = {
    artist: 'IU',
    album: 'lilac',
    show: function() {
        console.log(`${this.album} 앨범의 가수는 ${this.artist} 입니다.`);
    }
}

  1. 다른 함수의 인자로 전달될 수 있다.
    call 변수에 callMe() 함수를 할당해줬고, setInterval()에 callMe() 함수를 인자로 넣어줬다.
    setInterval()을 호출하게 되면 1초마다 callMe() 함수가 실행된다.
let call = function callMe() {
    console.log('호출 합니다.');
}


setInterval(call, 1000);
  1. 다른 함수의 결과로써 리턴될 수 있다.
    getScore() 함수의 리턴 값은 add()함수이다. getScore()함수를 호출하면 add()함수가 실행되고 결과값이 getScore()의 리턴 값이 된다.
let name = 'Jimi';
function getScore () {
  let num1 = 2,
      num2 = 3;

  function add() {
    return name + " scored " + (num1 + num2);
  }

  return add();
}

getScore(); // "Jimi scored 5"

이렇듯 함수는 변수에 저장될 수도 있고, 함수의 인자로 전달되거나 함수 내에서 리턴값으로 사용될 수 있다. 위 특징을 가진 함수로 하나 이상의 함수를 인수로 받거나 함수를 반환하는 고차함수를 만들 수 있다.

고차함수는 또 어떤건지 알아보자🤗

고차함수(higher order function)란

고차 함수는 함수를 인자(argument)로 받거나 함수를 리턴하는 함수를 의미한다.

이 때 다른 함수(caller)의 인자(argument)로 전달되는 함수를 콜백 함수(callback function)라고 한다.

콜백 함수를 전달받은 함수는 이 콜백 함수를 호출(invoke)할 수 있다. caller는 조건에 따라 콜백 함수의 실행 여부를 결정할 수도 있고, 여러 번 실행할 수 있다. 특히 콜백 함수는 어떤 작업이 완료되었을 때 호출되는 경우가 많아서 '답신 전화📞'를 뜻하는 콜백이라는 이름이 붙었다.

함수를 인자로 받는 경우와 리턴하는 경우를 봐보자.

  • 다른 함수를 인자로 받는 경우
    doubleNum은 다른 함수를 인자로 받는 고차 함수이다.
    함수 doubleNum의 첫 번째 인자 func에 함수가 들어올 경우 func은 콜백함수이다.
function double(num) {
  return num * 2;
}

function doubleNum(func, num) {
  return func(num);
}

// 아래와 같은 경우, 함수 double은 함수 doubleNum의 콜백 함수이다.
let output = doubleNum(double, 4);
console.log(output); // -> 8
  • 함수를 리턴하는 경우
    adder()는 익명 함수를 리턴하는 고차함수이다.
    리턴되는 익명 함수는 num인자를 받아서 added와 더한 값을 리턴한다.
function adder(added) {
  return function (num) {
    return num + added;
  };
}

// adder(5)는 함수이므로 함수 호출 연산자 '()'를 사용할 수 있다.
let output = adder(5)(3); // -> 8
console.log(output); // -> 8

아래와 같이 adder()함수를 변수에 할당하고, 그 변수를 output에 할당해서 함수를 호출할 수도 있다.

const add3 = adder(3);
output = add3(2);
console.log(output); // -> 5
  • 함수를 인자로 받고, 함수를 리턴하는 경우
    doubleAdder는 함수를 인자로 받고, 리턴값을 함수로 갖는 고차함수이다.
    func인자에 double()이 전달되면 double()는 콜백함수가 된다.
    변수 doubled는 double(added)를 할당받는다.
    리턴되는 익명 함수의 리턴값은 numdouble(added) 함수 실행 값을 더한 값이기 때문에 double(added)가 실행되며 num*2 값이 리턴된다.
function double(num) {
  return num * 2;
}

function doubleAdder(added, func) {
  const doubled = func(added);
  return function (num) {
    return num + doubled;
  };
}

// doubleAdder(5, double)는 함수이므로 함수 호출 기호 '()'를 사용한다.
doubleAdder(5, double)(3); // -> 13

// doubleAdder가 리턴하는 함수를 변수에 저장할 수 있다. (일급 객체 특징)
const addTwice3 = doubleAdder(3, double);
addTwice3(2); // --> 8

Array의 내장 고차 함수

자바스크립트에는 기본적으로 내장(built-in)되어 있는 고차 함수들이 있다. 배열 메서드 중 forEach(), find(), filter(), map(), reduce(), sort(), some(), every() 등이 해당된다.

1) filter()

배열의 요소 중 특정 조건을 만족하는 요소들만을 걸러서(filter) 새로운 배열을 만드는 메서드이다.

구문

arr.filter(callback func(element, index, arr));

이때, 특정 조건은 함수 형태로 filter()의 인자로 전달되어야 한다.

filter()를 써서 수(number)를 요소로 갖는 배열 중 짝수만을 걸러내거나, 18 보다 작은 수만을 걸러내는 식입니다. 문자열(string)을 요소로 갖는 배열 중 길이가 10 이하인 문자열만 걸러내거나, 'korea'만 걸러낼 수 있다.

배열의 요소 중 짝수만 뽑아 새배열을 만든다고 한다면 filter()를 쓸 수 있다.

  • 수도코드
    // 배열의 각 요소 : number 타입의 정수
    // 특정 논리(함수) : 숫자가 짝수이다. (true / false)
    // 따로 분류 : 짝수인 수

배열의 요소들이 el 인자값으로 들어가며, 반복문 처럼 요소들을 순회하며, 조건식이 true인 요소들을 뽑아 새로운 배열을 만든다.

let arr = [1, 2, 3, 4];
let output = arr.filter(function(el) {
	return el % 2 === 0;
});
console.log(output); // ->> [2, 4]

filter()의 동작을 쪼개어 나타내보면 아래와 같다.
배열과 특정 조건을 갖는 함수를 인자로 받아 배열의 요소를 순회하며 함수 실행 결과가 true일 때, 새로운 배열에 push해주고, 최종적으로 새로운 배열을 리턴해준다.

let arr = [1, 2, 3, 4];
// 배열의 filter 메소드는 함수를 인자로 받는 고차 함수이다.
arr.filter = function (arr, func) {
  const newArr = [];
  for (let i = 0; i < arr.length; i++) {
    if (func(arr[i]) === true) {
      newArr.push(this[i]);
    }
  }
  return newArr;
};

map()이나 reduce()도 반복문을 내포하고 있는 메서드들로 배열의 요소들을 순회하게 된다.

2) map()

map()은 배열의 모든 요소에 동일한 조건을 적용한 값을 요소로 갖는 새로운 배열을 반환한다.
이때, 조건은 함수로 작성되어야하며, 기존 배열을 수정하지 않는다.

filter()와 다른게 콜백 함수로 작성한 특정 조건을 모든 요소에 적용시킨 값을 반환한다.

map()은 이렇게 하나의 데이터를 다른 데이터로 맵핑(mapping) 할 때 주로 사용한다.

만화책 식객 27권의 정보가 배열에 담겨져 있다. 각 책의 부제(subtitle)만 담은 배열을 만들고 싶다. 이때 map()을 쓸 수 있다.

  • 수도코드
    // 배열의 각 요소 : 각 식객 1- 27권의 정보
    // 특정 논리(함수) : 책 한 권의 부제를 찾는다.
    // 다른 요소로 지정 : 각 식객 1- 27권의 부제
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); // 부제의 모음

3) reduce()

reduce()는 배열을 콜백함수를 실행하여 하나의 결과 값으로 반환한다.

구문

arr.reduce(callback func(acc, cur, idx, arr), initialValue)

reduce()는 초기값(initialValue)을 정할 수 있다. 정하지 않으면 배열의 제일 첫번째 요소가 초기값이 된다.

reduce()에는 누적값(accumulator)이란 개념이 나오는데
이 처음 누적값은 초기값이 된다.

그리고 이 메서드의 반환 값이 누적값(acc)가 된다.
현재 처리할 요소(cur)에 call back 함수의 실행 결과가 누적값에 할당되고, 누적값은 순회 중 유지되므로 결국 최종 결과는 하나의 값이 된다.

reduce는 문자열이나 숫자를 합치거나 뺄 수도 있고 제일 작은것 혹은 큰것을 비교할 수도 있고 배열이외에 다른 형태로도 만들 수 있다. 이 외에도 활용도가 높다.

reduce()를 어떻게 사용하는지 예시를 살펴보자.

  • 배열의 요소들 더한 값
const arr = [1, 2, 3, 4, 5];

let sum = arr.reduce(function(acc,cur) {
  return acc + cur;
});

sum; // 15
  • 배열을 문자열로
    수도코드
    // 배열의 각 요소 : 유져 정보
    // 응축하는 방법 (함수) : 하나의 유져의 이름과 쉼표를 이어붙인다(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, ''); // "Tim, Satya, Sundar, "

위 코드를 보면 reduce의 콜백 함수로 joinName()이 들어가고, 초기값을 빈문자열('')로 넣어줬다.
joinName()에 처음 resultStr의 인자로 ''과, users 배열이 들어가 실행되어진다. 언제까지? users 배열의 길이만큼

  • 배열을 객체로
    수도코드
    // 배열의 각 요소 : 유져 정보
    // 응축하는 방법 (함수) : 한 유저 이름의 첫 글자를 주소록 객체 속성의 키(key)로, 유저 정보를 주소록 객체 속성의 값(value)으로 추가
    // 원하는 형태 : 주소록 객체에 누적한다.
    // 응축된 결과 : 모든 유져의 정보가 알파벳으로 구분된 주소록
function makeAddressBook(addressBook, user) {  //addressBook 초기 인자값 : {}  > 배열의 요소들
  let firstLetter = user.name[0];	//이름의 첫 문자만 변수에 할당

  if(firstLetter in addressBook) { //객체에 firstLetter 키가 존재하면 
    addressBook[firstLetter].push(user);  //키값이 배열이니깐 배열에 push
  } else {
    addressBook[firstLetter] = []; // 키값은 빈배열
    addressBook[firstLetter].push(user); //키값에 push
  }

  return addressBook;
}

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

users.reduce(makeAddressBook, {});
// {T: Array(1), S: Array(2)}
// S: Array(2)
// 0: {name: "Satya", age: 30}
// 1: {name: "Sundar", age: 50}
// T: Array(1)
// 0: {name: "Tim", age: 40}

📌 for문과 ruduce()의 차이

for문은 break라는 예약어가 있기 때문에 중간에 반복을 끊어줄 수 있지만 reduce()는 중간에 빠져나올 수 없다.

마무리

마지막으로 고차함수를 찾아보며, 단번에 이해할 수 있언던 이미지로 마무리!

0개의 댓글