[JavaScript] 고차 함수
JavaScript 프로그래밍의 단계를 한 단계 높이는 방법에 대해서 학습합니다. 산 정상에서 내려다보면 더 넓은 시야를 가질 수 있습니다. 프로그래밍도 마찬가지로 높은 수준에서 생각하면, 높은 수준의 결과물이 나올 수 있습니다. 고차 함수를 통해 높은 수준에서 사고하는 방식을 학습합니다. 그리고 지금까지 학습한 내용을 종합하여, 좀 더 복잡한 알고리즘을 직접 구현할 수 있습니다.
Before You Learn
- JavaScript 기초 문법
- 타입
- 원시 자료형과 구분되는 참조 자료형의 특징에 대해 이해해야 합니다.
- 함수
- 함수에 대한 전반적인 이해가 필요합니다.
- 배열
- 배열 요소의 조회, 추가, 변경, 삭제를 할 수 있어야 합니다.
- 반복문을 이용하여 배열의 모든 요소를 순회할 수 있어야 합니다.
- 타입
Section1에서 학습한 JavaScript 기초는 앞으로 학습할 고차 함수에 필요한 기초 지식이므로, 개념을 충분하게 숙지해야 합니다. 함수에 대한 기초 개념이 부족할 경우 고차 함수를 이해하는 데 어려움을 겪을 수 있으니 반드시 숙지했는지 점검해 주세요!
일급 객체
비행기에는 퍼스트 클래스(first-class)가 있습니다. 이코노미 클래스와는 탑승 수속부터 기내식, 수하물이 나오는 순서까지 항공사와 관련된 모든 부분에서 혜택이 다릅니다. 퍼스트 클래스 좌석을 구매한 사람은 비싼 가격을 치르고, 항공사로부터 특별한 대우를 받습니다.
JavaScript에도 특별한 대우를 받는 일급 객체(first-class citizen)가 있습니다. 대표적인 일급 객체 중 하나가 함수입니다. JavaScript에서 함수는 아래와 같이 특별하게 취급됩니다.
- 변수에 할당(assignment)할 수 있다.
- 다른 함수의 전달인자(argument)로 전달될 수 있다.
- 다른 함수의 결과로써 리턴될 수 있다.
함수를 변수에 할당할 수 있기 때문에, 함수를 배열의 요소나 객체의 속성 값으로 저장할 수 있습니다. 함수를 데이터(string, number, boolean, array, object)처럼 다룰 수 있습니다.
- 변수에 함수를 할당하는 경우
/*
* 아래는 변수 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
함수는 변수에 저장된 데이터를 전달 인자로 받거나, 리턴 값으로 사용할 수 있습니다. 함수도 변수에 저장될 수 있기 때문에 함수를 인자로 받거나, 리턴 값으로 사용할 수 있습니다. 이어지는 콘텐츠를 통해 JavaScript의 고급 주제, 고차 함수(higher order function)를 학습합니다.
고차 함수의 이해
고차 함수(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;
};
}
function add(num, added) {
return num + added;
);
function adder(added) {
return add;
}
/*
* 함수 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
내장 고차 함수
내장 고차 함수 이해하기
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', 'javasprin', '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;
};
function isEven(num) {
return num % 2 === 0;
}
arr.filter(arr, isEven);
/*
* 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', 'javaspring', 'happy', 'hacking'];
// output = arr.filter(길이 5 이하)
// '길이 5 이하'를 판별하는 함수가 조건으로서 filter 메서드의 전달인자로 전달됩니다.
let output = arr.filter(isLteFive);
console.log(output); // ->> ['hello', 'code', 'happy']
arr.filter()
Chapter2-2. 내장 고차 함수 filter
실제 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,
bookType: 'cartoon',
title: '식객',
subtitle: '김치',
createdAt: '2003-12-15',
genre: '요리',
artist: '허영만',
averageScore: 9.55,
},
{
id: 3,
bookType: 'cartoon',
title: '식객',
subtitle: '장인의 손길',
createdAt: '2004-02-01',
genre: '요리',
artist: '허영만',
averageScore: 9.60,
},
{
id: 4,
bookType: 'cartoon',
title: '식객',
subtitle: '가을의 맛',
createdAt: '2004-05-10',
genre: '요리',
artist: '허영만',
averageScore: 9.45,
},
{
id: 5,
bookType: 'cartoon',
title: '식객',
subtitle: '명인의 솜씨',
createdAt: '2004-08-19',
genre: '요리',
artist: '허영만',
averageScore: 9.70,
},
{
id: 6,
bookType: 'cartoon',
title: '식객',
subtitle: '한국의 맛',
createdAt: '2004-11-25',
genre: '요리',
artist: '허영만',
averageScore: 9.75,
},
{
id: 7,
bookType: 'cartoon',
title: '식객',
subtitle: '전통의 힘',
createdAt: '2005-02-14',
genre: '요리',
artist: '허영만',
averageScore: 9.50,
},
{
id: 8,
bookType: 'cartoon',
title: '식객',
subtitle: '계절의 맛',
createdAt: '2005-06-03',
genre: '요리',
artist: '허영만',
averageScore: 9.55,
},
{
id: 9,
bookType: 'cartoon',
title: '식객',
subtitle: '가정의 맛',
createdAt: '2005-09-12',
genre: '요리',
artist: '허영만',
averageScore: 9.62,
},
{
id: 10,
bookType: 'cartoon',
title: '식객',
subtitle: '시골의 맛',
createdAt: '2005-12-20',
genre: '요리',
artist: '허영만',
averageScore: 9.48,
},
{
id: 11,
bookType: 'cartoon',
title: '식객',
subtitle: '추억의 맛',
createdAt: '2006-03-01',
genre: '요리',
artist: '허영만',
averageScore: 9.53,
},
{
id: 12,
bookType: 'cartoon',
title: '식객',
subtitle: '한식의 비밀',
createdAt: '2006-06-10',
genre: '요리',
artist: '허영만',
averageScore: 9.58,
},
{
id: 13,
bookType: 'cartoon',
title: '식객',
subtitle: '봄의 향기',
createdAt: '2006-09-09',
genre: '요리',
artist: '허영만',
averageScore: 9.60,
},
{
id: 14,
bookType: 'cartoon',
title: '식객',
subtitle: '겨울의 맛',
createdAt: '2006-12-15',
genre: '요리',
artist: '허영만',
averageScore: 9.57,
},
{
id: 15,
bookType: 'cartoon',
title: '식객',
subtitle: '우리의 맛',
createdAt: '2007-02-01',
genre: '요리',
artist: '허영만',
averageScore: 9.59,
},
{
id: 16,
bookType: 'cartoon',
title: '식객',
subtitle: '봄날의 맛',
createdAt: '2007-05-10',
genre: '요리',
artist: '허영만',
averageScore: 9.62,
},
{
id: 17,
bookType: 'cartoon',
title: '식객',
subtitle: '명절의 맛',
createdAt: '2007-08-19',
genre: '요리',
artist: '허영만',
averageScore: 9.55,
},
{
id: 18,
bookType: 'cartoon',
title: '식객',
subtitle: '여름의 맛',
createdAt: '2007-11-25',
genre: '요리',
artist: '허영만',
averageScore: 9.53,
},
{
id: 19,
bookType: 'cartoon',
title: '식객',
subtitle: '한식의 비밀',
createdAt: '2008-02-14',
genre: '요리',
artist: '허영만',
averageScore: 9.55,
},
{
id: 20,
bookType: 'cartoon',
title: '식객',
subtitle: '가족의 맛',
createdAt: '2008-06-03',
genre: '요리',
artist: '허영만',
averageScore: 9.58,
},
{
id: 21,
bookType: 'cartoon',
title: '식객',
subtitle: '이웃의 맛',
createdAt: '2008-09-12',
genre: '요리',
artist: '허영만',
averageScore: 9.60,
},
{
id: 22,
bookType: 'cartoon',
title: '식객',
subtitle: '한국의 맛',
createdAt: '2008-12-20',
genre: '요리',
artist: '허영만',
averageScore: 9.62,
},
{
id: 23,
bookType: 'cartoon',
title: '식객',
subtitle: '시골의 맛',
createdAt: '2009-03-01',
genre: '요리',
artist: '허영만',
averageScore: 9.65,
},
{
id: 24,
bookType: 'cartoon',
title: '식객',
subtitle: '계절의 맛',
createdAt: '2009-06-10',
genre: '요리',
artist: '허영만',
averageScore: 9.68,
},
{
id: 25,
bookType: 'cartoon',
title: '식객',
subtitle: '추억의 맛',
createdAt: '2009-09-09',
genre: '요리',
artist: '허영만',
averageScore: 9.60,
},
{
id: 26,
bookType: 'cartoon',
title: '식객',
subtitle: '명인의 솜씨',
createdAt: '2009-12-15',
genre: '요리',
artist: '허영만',
averageScore: 9.62,
},
{
id: 27,
bookType: 'cartoon',
title: '식객',
subtitle: '전통의 힘',
createdAt: '2010-02-01',
genre: '요리',
artist: '허영만',
averageScore: 9.55,
}
];
// 단행본 한 권의 출판 연도가 2003인지 확인하는 함수
const isCreatedAt2003 = function (cartoon, year) {
// 'createdAt' 값을 Date 객체로 변환하고 연도를 추출
const fullYear = new Date(cartoon.createdAt).getFullYear();
// 연도가 2003년인지 확인
return fullYear === year;
};
// 출판 연도가 2003년인 책의 모음
const filteredCartoons = cartoons.filter(isCreatedAt2003, 2003);
// 결과 출력
console.log(filteredCartoons);
Chapter2-3. 내장 고차 함수 map
실제 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,
bookType: 'cartoon',
title: '식객',
subtitle: '김치',
createdAt: '2003-12-15',
genre: '요리',
artist: '허영만',
averageScore: 9.55,
},
{
id: 3,
bookType: 'cartoon',
title: '식객',
subtitle: '장인의 손길',
createdAt: '2004-02-01',
genre: '요리',
artist: '허영만',
averageScore: 9.60,
},
{
id: 4,
bookType: 'cartoon',
title: '식객',
subtitle: '가을의 맛',
createdAt: '2004-05-10',
genre: '요리',
artist: '허영만',
averageScore: 9.45,
},
{
id: 5,
bookType: 'cartoon',
title: '식객',
subtitle: '명인의 솜씨',
createdAt: '2004-08-19',
genre: '요리',
artist: '허영만',
averageScore: 9.70,
},
{
id: 6,
bookType: 'cartoon',
title: '식객',
subtitle: '한국의 맛',
createdAt: '2004-11-25',
genre: '요리',
artist: '허영만',
averageScore: 9.75,
},
{
id: 7,
bookType: 'cartoon',
title: '식객',
subtitle: '전통의 힘',
createdAt: '2005-02-14',
genre: '요리',
artist: '허영만',
averageScore: 9.50,
},
{
id: 8,
bookType: 'cartoon',
title: '식객',
subtitle: '계절의 맛',
createdAt: '2005-06-03',
genre: '요리',
artist: '허영만',
averageScore: 9.55,
},
{
id: 9,
bookType: 'cartoon',
title: '식객',
subtitle: '가정의 맛',
createdAt: '2005-09-12',
genre: '요리',
artist: '허영만',
averageScore: 9.62,
},
{
id: 10,
bookType: 'cartoon',
title: '식객',
subtitle: '시골의 맛',
createdAt: '2005-12-20',
genre: '요리',
artist: '허영만',
averageScore: 9.48,
},
{
id: 11,
bookType: 'cartoon',
title: '식객',
subtitle: '추억의 맛',
createdAt: '2006-03-01',
genre: '요리',
artist: '허영만',
averageScore: 9.53,
},
{
id: 12,
bookType: 'cartoon',
title: '식객',
subtitle: '한식의 비밀',
createdAt: '2006-06-10',
genre: '요리',
artist: '허영만',
averageScore: 9.58,
},
{
id: 13,
bookType: 'cartoon',
title: '식객',
subtitle: '봄의 향기',
createdAt: '2006-09-09',
genre: '요리',
artist: '허영만',
averageScore: 9.60,
},
{
id: 14,
bookType: 'cartoon',
title: '식객',
subtitle: '겨울의 맛',
createdAt: '2006-12-15',
genre: '요리',
artist: '허영만',
averageScore: 9.57,
},
{
id: 15,
bookType: 'cartoon',
title: '식객',
subtitle: '우리의 맛',
createdAt: '2007-02-01',
genre: '요리',
artist: '허영만',
averageScore: 9.59,
},
{
id: 16,
bookType: 'cartoon',
title: '식객',
subtitle: '봄날의 맛',
createdAt: '2007-05-10',
genre: '요리',
artist: '허영만',
averageScore: 9.62,
},
{
id: 17,
bookType: 'cartoon',
title: '식객',
subtitle: '명절의 맛',
createdAt: '2007-08-19',
genre: '요리',
artist: '허영만',
averageScore: 9.55,
},
{
id: 18,
bookType: 'cartoon',
title: '식객',
subtitle: '여름의 맛',
createdAt: '2007-11-25',
genre: '요리',
artist: '허영만',
averageScore: 9.53,
},
{
id: 19,
bookType: 'cartoon',
title: '식객',
subtitle: '한식의 비밀',
createdAt: '2008-02-14',
genre: '요리',
artist: '허영만',
averageScore: 9.55,
},
{
id: 20,
bookType: 'cartoon',
title: '식객',
subtitle: '가족의 맛',
createdAt: '2008-06-03',
genre: '요리',
artist: '허영만',
averageScore: 9.58,
},
{
id: 21,
bookType: 'cartoon',
title: '식객',
subtitle: '이웃의 맛',
createdAt: '2008-09-12',
genre: '요리',
artist: '허영만',
averageScore: 9.60,
},
{
id: 22,
bookType: 'cartoon',
title: '식객',
subtitle: '한국의 맛',
createdAt: '2008-12-20',
genre: '요리',
artist: '허영만',
averageScore: 9.62,
},
{
id: 23,
bookType: 'cartoon',
title: '식객',
subtitle: '시골의 맛',
createdAt: '2009-03-01',
genre: '요리',
artist: '허영만',
averageScore: 9.65,
},
{
id: 24,
bookType: 'cartoon',
title: '식객',
subtitle: '계절의 맛',
createdAt: '2009-06-10',
genre: '요리',
artist: '허영만',
averageScore: 9.68,
},
{
id: 25,
bookType: 'cartoon',
title: '식객',
subtitle: '추억의 맛',
createdAt: '2009-09-09',
genre: '요리',
artist: '허영만',
averageScore: 9.60,
},
{
id: 26,
bookType: 'cartoon',
title: '식객',
subtitle: '명인의 솜씨',
createdAt: '2009-12-15',
genre: '요리',
artist: '허영만',
averageScore: 9.62,
},
{
id: 27,
bookType: 'cartoon',
title: '식객',
subtitle: '전통의 힘',
createdAt: '2010-02-01',
genre: '요리',
artist: '허영만',
averageScore: 9.55,
}
];
// 만화책 한 권의 부제를 리턴하는 함수
const findSubtitle = function (cartoon) {
// 만화책 객체에서 'subtitle' 속성의 값을 리턴
return cartoon.subtitle;
};
// 각 책의 부제 모음
const subtitles = cartoons.map(findSubtitle); // ['어머니의 쌀', '김치', ..., '전통의 힘']
// 결과 출력
console.log(subtitles);
Chpater2-4. 내장 고차 함수 reduce
실제 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,
bookType: 'cartoon',
title: '식객',
subtitle: '김치',
createdAt: '2003-12-15',
genre: '요리',
artist: '허영만',
averageScore: 9.55,
},
{
id: 3,
bookType: 'cartoon',
title: '식객',
subtitle: '장인의 손길',
createdAt: '2004-02-01',
genre: '요리',
artist: '허영만',
averageScore: 9.60,
},
{
id: 4,
bookType: 'cartoon',
title: '식객',
subtitle: '가을의 맛',
createdAt: '2004-05-10',
genre: '요리',
artist: '허영만',
averageScore: 9.45,
},
{
id: 5,
bookType: 'cartoon',
title: '식객',
subtitle: '명인의 솜씨',
createdAt: '2004-08-19',
genre: '요리',
artist: '허영만',
averageScore: 9.70,
},
{
id: 6,
bookType: 'cartoon',
title: '식객',
subtitle: '한국의 맛',
createdAt: '2004-11-25',
genre: '요리',
artist: '허영만',
averageScore: 9.75,
},
{
id: 7,
bookType: 'cartoon',
title: '식객',
subtitle: '전통의 힘',
createdAt: '2005-02-14',
genre: '요리',
artist: '허영만',
averageScore: 9.50,
},
{
id: 8,
bookType: 'cartoon',
title: '식객',
subtitle: '계절의 맛',
createdAt: '2005-06-03',
genre: '요리',
artist: '허영만',
averageScore: 9.55,
},
{
id: 9,
bookType: 'cartoon',
title: '식객',
subtitle: '가정의 맛',
createdAt: '2005-09-12',
genre: '요리',
artist: '허영만',
averageScore: 9.62,
},
{
id: 10,
bookType: 'cartoon',
title: '식객',
subtitle: '시골의 맛',
createdAt: '2005-12-20',
genre: '요리',
artist: '허영만',
averageScore: 9.48,
},
{
id: 11,
bookType: 'cartoon',
title: '식객',
subtitle: '추억의 맛',
createdAt: '2006-03-01',
genre: '요리',
artist: '허영만',
averageScore: 9.53,
},
{
id: 12,
bookType: 'cartoon',
title: '식객',
subtitle: '한식의 비밀',
createdAt: '2006-06-10',
genre: '요리',
artist: '허영만',
averageScore: 9.58,
},
{
id: 13,
bookType: 'cartoon',
title: '식객',
subtitle: '봄의 향기',
createdAt: '2006-09-09',
genre: '요리',
artist: '허영만',
averageScore: 9.60,
},
{
id: 14,
bookType: 'cartoon',
title: '식객',
subtitle: '겨울의 맛',
createdAt: '2006-12-15',
genre: '요리',
artist: '허영만',
averageScore: 9.57,
},
{
id: 15,
bookType: 'cartoon',
title: '식객',
subtitle: '우리의 맛',
createdAt: '2007-02-01',
genre: '요리',
artist: '허영만',
averageScore: 9.59,
},
{
id: 16,
bookType: 'cartoon',
title: '식객',
subtitle: '봄날의 맛',
createdAt: '2007-05-10',
genre: '요리',
artist: '허영만',
averageScore: 9.62,
},
{
id: 17,
bookType: 'cartoon',
title: '식객',
subtitle: '명절의 맛',
createdAt: '2007-08-19',
genre: '요리',
artist: '허영만',
averageScore: 9.55,
},
{
id: 18,
bookType: 'cartoon',
title: '식객',
subtitle: '여름의 맛',
createdAt: '2007-11-25',
genre: '요리',
artist: '허영만',
averageScore: 9.53,
},
{
id: 19,
bookType: 'cartoon',
title: '식객',
subtitle: '한식의 비밀',
createdAt: '2008-02-14',
genre: '요리',
artist: '허영만',
averageScore: 9.55,
},
{
id: 20,
bookType: 'cartoon',
title: '식객',
subtitle: '가족의 맛',
createdAt: '2008-06-03',
genre: '요리',
artist: '허영만',
averageScore: 9.58,
},
{
id: 21,
bookType: 'cartoon',
title: '식객',
subtitle: '이웃의 맛',
createdAt: '2008-09-12',
genre: '요리',
artist: '허영만',
averageScore: 9.60,
},
{
id: 22,
bookType: 'cartoon',
title: '식객',
subtitle: '한국의 맛',
createdAt: '2008-12-20',
genre: '요리',
artist: '허영만',
averageScore: 9.62,
},
{
id: 23,
bookType: 'cartoon',
title: '식객',
subtitle: '시골의 맛',
createdAt: '2009-03-01',
genre: '요리',
artist: '허영만',
averageScore: 9.65,
},
{
id: 24,
bookType: 'cartoon',
title: '식객',
subtitle: '계절의 맛',
createdAt: '2009-06-10',
genre: '요리',
artist: '허영만',
averageScore: 9.68,
},
{
id: 25,
bookType: 'cartoon',
title: '식객',
subtitle: '추억의 맛',
createdAt: '2009-09-09',
genre: '요리',
artist: '허영만',
averageScore: 9.60,
},
{
id: 26,
bookType: 'cartoon',
title: '식객',
subtitle: '명인의 솜씨',
createdAt: '2009-12-15',
genre: '요리',
artist: '허영만',
averageScore: 9.62,
},
{
id: 27,
bookType: 'cartoon',
title: '식객',
subtitle: '전통의 힘',
createdAt: '2010-02-01',
genre: '요리',
artist: '허영만',
averageScore: 9.55,
}
];
// 단행본 한 권의 평점을 누적값에 더한다.
const scoreReducer = function (sum, cartoon) {
return sum + cartoon.averageScore;
};
// 초기값에 0을 주고, 숫자의 형태로 평점을 누적한다.
let initialValue = 0
// 모든 책의 평점을 누적한 평균을 구한다.
const cartoonsAvgScore = cartoons.reduce(scoreReducer, initialValue) / cartoons.length;
[코드] 배열의 reduce 메서드로 모든 책의 누적 평균을 구할 수 있습니다.
reduce는 배열 요소의 합을 구할 때만 사용하는 것이 아닙니다. 이어서 소개하는 사용법을 참고하여, reduce를 더욱 풍부하게 사용할 수 있습니다.
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, '');
[코드] 콜백 함수 joinName은 users 배열 안에 있는 요소의 이름을 하나로 응축합니다.
실습
reduce의 콜백 함수 joinName은 총 몇 번 실행되나요? 다음 표에 들어가는 값은 각각 무엇인가요?
호출 횟수| resultStr| user| 리턴 값-------- | --------- | --------- | ---------
1번째 호출 | `______` | `______` | `______`2번째 호출 | `______` | `______` | `______`n번째 호출 | `______` | `______` | `______`
최종 리턴 값: ______
배열을 객체로
수도 코드
- 배열의 각 요소 : 유저 정보
- 응축하는 방법 (함수) : 유저 한 명의 이름 중 첫 글자를 주소록 객체 속성의 키(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, {});
[코드] 콜백 함수 makeAddressBook은 users 배열 안에 있는 요소로 주소록을 만듭니다.
실습
reduce의 콜백 함수 makeAddressBook은 총 몇 번 실행되나요? 다음 표에 들어가는 값은 각각 무엇인가요?
호출 횟수| addressBook| user| 리턴 값-------- | --------- | --------- | ---------
1번째 호출 | `______` | `______` | `______`2번째 호출 | `______` | `______` | `______`n번째 호출 | `______` | `______` | `______`
최종 리턴 값
{
T: [
{ name: 'Tim', age: 40 }
],
S: [
{ name: 'Satya', age: 30 },
{ name: 'Sundar', age: 50 }
]
}
고차 함수가 필요 이유
높은 수준에서 생각하기
컴퓨터 공학의 근간을 이루는 개념은 여러 가지가 있지만, 이번 콘텐츠에서는 추상화(abstraction)를 설명합니다. 복잡한 어떤 것을 압축해서 핵심만 추출한 상태로 만드는 것이 추상화입니다. 우리가 살아가는 이 세상은, 추상화로 가득 차 있습니다. '-1'을 표현하는 현실의 방법은 존재하지 않습니다. 그러나 우리는 '-1'이라는 문자를 보고, "-1은 0보다 1만큼 작은 수다."라고 설명할 수 있습니다. 이렇듯, 인간은 추상화를 통해 생각하고 표현합니다. 추상화를 이용하면, 효율적이고 편하게 생각할 수 있기 때문입니다.
브라우저 창에 주소를 입력했을 때, 어떤 일이 일어나는지 정확하게 알고 있나요? 입력한 내용을 전파하고, 어디 서버로 갔다가 다른 서버로 가는 등 그런 복잡한 내용을, 일상생활에서는 몰라도 됩니다. 우리는 그저 주소창에 올바른 주소를 입력하면, 브라우저가 해당 사이트를 보여 준다는 것만 알고 있습니다. 스마트폰으로 카카오톡이나 페이스북 메신저를 통해 친구에게 '안녕?'이란 메시지를 보내면, 그 순간 여러분들의 스마트폰은 기지국과 약 20개의 메시지를 주고받습니다. 하지만 우린 이런 것들을 전부 알지 못하고, 알 필요도 없습니다. 그러나 입력창에 메시지를 입력하고 전송 버튼을 누르면, 내 친구가 메시지를 받는다는 사실은 알고 있습니다. 자동차의 시동 버튼, 자료를 정리하는 엑셀, 지하철/버스를 타기 위한 교통 카드도 추상화의 결과입니다. 일상생활에서 추상화가 아닌 것을 찾아보기 힘들 정도입니다.
JavaScript를 비롯한 많은 프로그래밍 언어 역시, 추상화의 결과입니다. 컴퓨터를 구성하는 장치(중앙처리장치, CPU; Central Processing Unit)는 0과 1만 이해합니다. 크롬 개발자 도구의 콘솔(console) 탭에서 다음의 코드를 입력했을 때, 어떤 과정을 거쳐 10이 출력되는지 몰라도 10을 출력할 수 있습니다. 그런 복잡한 것들은 크롬의 JavaScript 해석기(엔진)가 대신해 주기 때문입니다.
function sum(num1, num2) {
return num1 + num2;
}
const output = sum(3, 7);
console.log(output); // --> 10
[코드] 개발자 도구의 콘솔에서 실행하면, 계산 결과를 출력합니다.
컴퓨터의 내부 구조에 대한 고민이 추상화로 해결되었습니다. 우리는 JavaScript의 문법(syntax)을 올바르게 사용하는 것만으로, JavaScript가 없었을 때보다 다양한 프로그램을 보다 쉽게 작성할 수 있습니다. 이처럼 고민거리가 줄어들고, 그래서 문제의 해결이 더 쉬워지는 것이 추상화의 이점입니다.
- 추상화 = 생산성(productivity)의 향상
한편 프로그램을 작성할 때, 자주 반복해서 사용하는 로직은 별도의 함수로 작성하기도 합니다. 이 역시 추상화의 좋은 사례입니다. 추상화의 관점에서 함수를 바라보면, 함수는 사고(thought) 또는 논리(logic)의 묶음입니다.
아래의 getAverage 함수는 number 타입을 요소로 갖는 배열을 입력받아, 모든 요소의 평균값을 리턴합니다. 앞으로는 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 함수는 배열을 인자로 받아, 평균값을 리턴합니다.
함수를 통해 얻은 추상화를, 한 단계 더 높인 것이 고차 함수입니다. getAverage 함수는 값(배열)을 전달받아, 이 값을 가지고 복잡한 작업을 수행합니다. 이는 값 수준에서의 추상화입니다.
- 함수 = 값을 전달받아 값을 리턴한다. = 값에 대한 복잡한 로직은 감추어져 있다. = 값 수준에서의 추상화
고차 함수는 이 추상화의 수준을 사고의 추상화 수준으로 끌어올립니다.
- 값 수준의 추상화: 단순히 값(value)을 전달받아 처리하는 수준
- 사고의 추상화: 함수(사고의 묶음)를 전달받아 처리하는 수준
다시 말해 고차 함수를 통해, 보다 높은 수준(higher order)에서 생각할 수 있습니다.
- 고차 함수 = 함수를 전달받거나 함수를 리턴한다. = 사고(함수)에 대한 복잡한 로직은 감추어져 있다. = 사고 수준에서의 추상화
추상화의 수준이 높아지면 생산성도 비약적으로 상승할 수 있습니다. 이어지는 콘텐츠에서 예시를 통해 살펴봅니다.
고차 함수와 추상화
HOFs: High Order Functions
사고 수준의 추상화의 예시
const data = [
{
gender: 'male',
age: 24,
},
{
gender: 'male',
age: 25,
},
{
gender: 'female',
age: 27,
},
{
gender: 'female',
age: 22,
},
{
gender: 'male',
age: 29,
},
];
[코드] 예시 데이터
위와 같이 주어진 데이터를 순차적으로 처리하려고 할 때, 모든 작업을 하나의 함수로 작성할 수 있습니다. 예를 들어 남성들의 평균 나이를 구한다고 할 때에는, 다음과 같이 함수를 작성할 수 있습니다.
function getAverageAgeOfMaleAtOnce(data) {
const onlyMales = data.filter(function (d) {
// data.filter는 배열의 각 요소에 인자로 전달받은 함수를 적용하고,
// 그 결과가 true인 요소만을 갖는 배열을 리턴합니다.
return d.gender === 'male';
});
const numOfMales = onlyMales.length;
const onlyMaleAges = onlyMales.map(function (d) {
// onlyMales.map는 배열의 각 요소에 인자로 전달받은 함수를 적용하고,
// 각 결과를 요소로 갖는 배열을 리턴합니다.
return d.age;
});
const sumOfAges = onlyMaleAges.reduce(function (acc, cur) {
// onlyMaleAges.reduce는 배열의 각 요소에 인자로 전달받은 함수를 적용하고,
// 각 결과를 두 번째 인자로 전달받은 초기값(0)에 누적한 결과를 리턴합니다.
return acc + cur;
}, 0);
return sumOfAges / numOfMales;
}
[코드] 남성들의 평균 나이를 구하는 하나의 함수 getAverageAgeOfMaleAtOnce
위에 제시된 getAverageAgeOfMaleAtOnce 함수는 배열 메서드를 적절하게 사용하여 순차적으로 원하는 작업을 수행합니다. 이 코드는 꽤 괜찮은 코드이지만, '남성'의 '평균 나이'만 구하는 작업에만 사용할 수 있습니다. 개선할 점을 찾아보면, 'male'을 매개변수화(parameterization) 하여 조금 더 일반적인(generic) 함수로 변경할 수 있습니다. 이렇게 수정하더라도, 어디까지나 '남성' 또는 '여성'의 '평균 나이'를 구하는 작업만 수행할 수 있습니다.
한편, filter, map, reduce 등의 배열 메서드는 다른 목적을 위해서 사용될 수도 있습니다. 예를 들어 '남성' 중 '최연소 나이'를 구하거나, '여성' 중 '최연소 나이와 최연장 나이의 차이'를 구할 때, 이미 작성된 로직을 그대로 쓸 수 있습니다.
추상화는 고차 함수를 통해, 보다 쉽게 달성할 수 있습니다. 아래의 compose 함수는 입력받은 함수를 순서대로 결합하는 고차 함수입니다. 각각의 작업(filter, map, reduce)은 별도의 함수로 분리되어, compose의 인자로 전달되는 콜백 함수가 됩니다.
function getOnlyMales(data) {
return data.filter(function (d) {
return d.gender === 'male';
});
}
function getOnlyAges(data) {
return data.map(function (d) {
return d.age;
});
}
function getAverage(data) {
const sum = data.reduce(function (acc, cur) {
return acc + cur;
}, 0);
return sum / data.length;
}
function compose(...funcArgs) {
// compose는 여러 개의 함수를 인자로 전달받아 함수를 리턴하는 고차 함수입니다.
// compose가 리턴하는 함수(익명 함수)는 임의의 타입의 data를 입력받아,
return function (data) {
// funcArgs의 요소인 함수들을 차례대로 적용(apply)시킨 결과를 리턴합니다.
let result = data;
for (let i = 0; i < funcArgs.length; i++) {
result = funcArgs[i](result);
}
return result;
};
}
// compose를 통해 함수들이 순서대로 적용된다는 것이 직관적으로 드러납니다.
// 각각의 함수는 다른 목적을 위해 재사용(reuse) 될 수 있습니다.
const getAverageAgeOfMale = compose(
getOnlyMales, // 배열을 입력받아 배열을 리턴하는 함수
getOnlyAges, // 배열을 입력받아 배열을 리턴하는 함수
getAverage // 배열을 입력받아 `number` 타입을 리턴하는 함수
);
const result = getAverageAgeOfMale(data);
console.log(result); // --> 26
[코드] 입력된 함수를 순차적으로 실행하는 고차 함수 compose
이처럼 고차 함수를 통해 사고 수준에서의 추상화를 달성할 수 있습니다. 각각의 작업은 다른 목적을 위해 재사용될 수 있습니다. 여러 함수를 작성하고, 새로운 작업의 조합을 만들어 보세요.
[JS] 비동기
이번에는 JavaScript의 가장 큰 장점 중 하나인 비동기 흐름을 학습합니다. 비동기 흐름은 callback, promise, async/await 중 하나의 문법을 이용하여 구현할 수 있습니다. 이번 유닛에서는 기존에 학습한 callback을 복습하고, promise와 async/await을 추가로 학습합니다.
고차함수 돌아보기
고차 함수(Higher order function) 개념을 복습합니다.
🌟 고차 함수 학습 자료로 돌아가서 복습해도 좋습니다.
- 고차 함수는 전달인자(argument)로 함수를 넘겨줄 수 있습니다.
- 고차 함수의 전달인자로 넘겨주는 함수를 콜백 함수(Callback function)라고 합니다. 콜백 함수는 어떤 작업 중에 호출하는 경우가 많아서, 업무 중 걸려온 전화에 답신하는 전화를 나타내는 콜백이라는 이름이 붙여졌습니다.
- 고차 함수는 다른 함수를 리턴할 수 있습니다.
- '함수를 리턴하는 함수'는 모양새가 특이한 만큼, 부르는 용어가 따로 있습니다. '함수를 리턴하는 함수'를 고안해 낸 논리학자 하스켈 커리(Haskell Curry)의 이름을 따, 커링 함수라고 합니다. 따로 커링 함수라는 용어를 사용하는 경우에는, 고차 함수란 용어를 '함수를 전달인자로 받는 함수'에만 한정해 사용하기도 합니다. 그러나 정확하게 구분하자면, 고차 함수가 커링 함수를 포함합니다.
- '함수를 리턴하는 함수'와 '함수를 전달인자로 받는 함수' 모두 고차 함수입니다.
- 즉, 고차 함수는 콜백 함수와 커링 함수의 상위 개념입니다.
콜백 함수를 전달받은 고차 함수(caller)는, 함수 내부에서 이 콜백 함수를 호출(invoke)할 수 있고, 조건에 따라 콜백 함수의 실행 여부를 결정할 수도 있습니다. 아예 호출하지 않을 수도 있고, 여러 번 실행할 수도 있습니다. 특정 작업의 완료 후에 호출하는 경우는 이후에 충분히 접할 수 있습니다.
과제 - Underbar
예전에는 배열 메서드가 브라우저에서 자체적으로 지원되지 않던 시절이 있었습니다. 이때 선배 개발자들은 보다 나은 방법으로 배열이나 객체를 다루기 위한 도구 모음집을 만들었는데, 이것을 후에 **라이브러리(Library)**라고 부르기 시작했습니다. 이번 과제에서는 배열, 객체를 다루는 Underbar라는 라이브러리를 직접 구현하면서 자바스크립트 내장 메서드가 어떻게 콜백(Callback) 함수를 활용하는지 원리부터 자세히 학습할 수 있습니다.
Underbar의 모티브가 되는 라이브러리는, underscore.js, lodash 등이 있습니다. 이 라이브러리는 여전히 JavaScript 생태계에서 인기 있는 라이브러리입니다. 여러분도 보다 나은 프로그래밍을 위한 라이브러리를 만들어 보고, 효율적이고 아름다운 프로그래밍으로 한 발자국 더 나아가길 기대합니다.
Before You Learn
- 고차 함수를 복습하여 고차 함수의 기본적인 사용법을 이해합니다.
- 콜백 함수를 전달하여 사용할 수 있습니다.
- 클로저 함수를 리턴하여 활용할 수 있습니다.
- ... (spread syntax)를 사용하여 전달인자(arguments)의 개수를 파악할 수 있고, 각각의 전달인자에 접근할 수 있어야 합니다.
학습 목표
- 자바스크립트 배열 내장 메서드(forEach, map, filter, reduce 등)의 원리를 이해한다.
- 콜백 함수 전달을 자유롭게 할 수 있다.
시작하기
Bare Minimum Requirement까지는 필수로 풀고, Advanced, Nightmare는 학습을 진행하며 선택적으로 하나씩 풀어봅니다.
Bare Minimum Requirement
'src/01_bareMinimum.js' 파일을 수정하여, 테스트를 통과합니다.
- 자주 사용되는 기본적인 JavaScript의 배열 메서드를 직접 구현하며 원리를 이해한다.(slice, forEach, indexOf, filter, map, reduce 등)
- 고차 함수(Higher order function)를 활용하여 기존에 만든 함수를 콜백 함수로 재사용할 수 있다.
- iteratee가 콜백 함수임을 이해할 수 있다.
주의 사항
- 이번 과제에서는 극히 일부의 배열 메서드만 사용할 수 있습니다. 자신이 만든 함수를 활용하여, 함수 위주로 코드를 작성해야 합니다.
- 'spec' 디렉토리의 테스트는 수정하지 마세요.
- 과제 제출 시, console.log를 제거하세요.
- console.log는 디버깅을 목적으로 코드 내부의 작동 방식을 파악하는 데에 도움이 됩니다. 그러나 console.log()를 남겨두고 배포하게 되면 문제가 생길 수 있습니다. 해당 메서드로 외부인이 코드 내부의 동작 방식을 쉽게 파악할 수 있기 때문입니다. 이는 해킹의 우려가 있을 수 있으므로 코드 내부에서 제거해야 합니다.
Advanced Challenge (Optional)
'src/02_advanced.js' 파일을 수정하여, 테스트를 통과합니다.
Advanced Challenge 진행을 위해서 아래 안내를 따르세요.
- **'.test/index.js'**의 xdescribe 함수 이름을 describe로 변경합니다.
- SpecRunner.html에서 주석을 해제하세요.
- Part 1에서 제작한 언더바 (_) 고차 함수를 응용하여 Part 2를 해결할 수 있다.
- 함수 커스텀 메서드 : once, delay (필요하면 **'비동기'**를 예습하세요.)
- 배열 내장 메서드 : includes, every, some
- 객체 커스텀 메서드 : extends, defaults
- 배열 커스텀 메서드 : zip, zipStrict, intersection, difference, shuffle
- rest parameter를 자유자재로 사용할 수 있다.
- _.shffule에서 immutable 개념을 복습할 수 있다.
Nightmare (Optional)
'03_nightmare.js'를 수정하여 Nightmare Test를 통과합니다.
- 'SpecRunner.html'을 실행했는데, Advanced Test가 보이지 않나요? 파일을 잘 살펴보고 주석을 제거하세요.
- 직접 구현한 코드를 underscore.js 라이브러리, 레퍼런스와 비교하고, 더 나은 코드를 작성하기 위해 노력합니다. 이런 과정을 리팩토링이라고 합니다.
01_bareMinimum.js
'use strict';
/**
* underbar 스프린트에 오신 것을 환영합니다!
*
* 여러분은 자바스크립트를 보다 효율적으로 사용하기 위해서 만들어진 underscore.js 라이브러리를
* 비슷하게 구현하면서 자바스크립트의 문법을 더욱 깊게 이해하는 시간을 가지게 됩니다.
*/
// _.identity는 전달인자(argument)가 무엇이든, 그대로 리턴합니다.
// 이 함수는 underbar의 기능 구현 및 테스트를 위해 재사용되는 함수입니다.
_.identity = function (val) {
// TODO: 여기에 코드를 작성합니다.
};
/**
* COLLECTIONS
* ===========
* collection은 영어로 '모음, 무리'라는 뜻입니다. 컴퓨터 과학에서는 '데이터(data, 자료)의 모음'으로 부를 수 있겠죠.
* 우리가 배웠던 collection의 종류는 무엇이 있을까요? 다양한 종류가 있지만 배열과 객체가 대표적입니다.
*
* 배열은 데이터들(요소, element)을 '순서대로' 모은 자료 구조입니다. 요소의 위치를 통해 데이터에 접근할 수 있습니다.
* 객체는 서로 관련있는 데이터들(속성, property)을 'key-value' 형태로 '순서 없이' 모은 자료 구조입니다. 속성에 부여된 키(key)를 통해 데이터에 접근할 수 있습니다.
*
* collection의 각 데이터를 다루는 것은 매우 흔한 작업 중에 하나입니다.
* 예를 들어, 아래의 작업들을 생각해 볼 수 있습니다.
* 배열의 요소 중 가장 큰 값을 찾는 것
* 배열의 모든 요소의 합을 구하는 것
* 객체에 특정 속성의 이름(key)이 존재하는지 확인하는 것
*
* 각 작업들은 collection의 각 데이터를 가지고 비슷한 처리를 합니다.
* 배열의 요소 중 가장 큰 값을 찾는 것 => 현재 데이터가 가장 큰 값인지 확인
* 배열의 모든 요소의 합을 구하는 것 => 현재 데이터를 누적값에 더하기
* 객체에 특정 속성의 이름(key)이 존재하는지 확인하는 것 => 현재 key가 원하는 값인지 확인하기
*
* 이처럼 비슷한 처리가 반복되기 때문에 이를 반복(iteration) 작업이라고 부를 수 있습니다.
* 자바스크립트는 이러한 반복(iteration) 작업을 위한 여러 수단들을 제공합니다.
* 반복문(for, for of, for in, while)과 반복을 위한 내장 메소드(arr.map, arr.filter)들이 대표적입니다.
* 사실 반복문의 제어 변수로 주로 사용하는 변수 i는 iteration의 앞 글자 i를 의미합니다.
* 아래의 반복문은 총 console.log가 총 세 번 반복됩니다.
* 이때, console.log와 같이 반복되는 작업을 iteratee(반복되는 것)라고 부를 수 있습니다.
* for (let i = 0; i < 3; i++) {
* console.log(i);
* }
*
* 이 반복문은 아래와 같이 해석됩니다.
* let i = 0;
* { // 0번째 반복
* console.log(0); // 변수 i가 0으로 치환됨
* i++; // i는 1이 됨
* }
*
* 1 < 3 이므로 다음 반복 작업을 실행
* { // 1번째 반복
* console.log(1); // 변수 i가 1로 치환됨
* i++; // i는 2이 됨
* }
*
* 2 < 3 이므로 다음 반복 작업을 실행
* { // 2번째 반복
* console.log(2); // 변수 i가 2로 치환됨
* i++; // i는 3이 됨
* }
*
* 3 < 3 이 아니므로 반복 작업 종료
*
*
*
*
* 이번 스프린트에서는 collection을 다루는 여러 함수를 직접 구현해 봅니다.
*
* IMPORTANT NOTE!
* ===========
* 아래에 _.slice가 이미 구현되어 있습니다. 이 함수를 가이드 삼아, 앞으로 나올 함수들을 구현해 보세요.
* 이번 스프린트에서 아래 예외를 제외하고 배열(Array), 집합(Set), 맵(Map)의 기본 메소드 사용은 금지되어 있습니다.
* 사용 가능한 내장 메소드: Array.prototype 의 'pop', 'push', 'shift', 'sort'
* 단, 새로운 함수를 구현할 때 이전에 구현한 함수를 활용해도 됩니다. (이미 해결한 문제를 또 해결할 필요는 없겠죠?)
* 사전에 이미 완료된 과제의 일부분을 만나게 될 경우, 반드시 코드를 잘 읽어 보고 이해하고 넘어가시기 바랍니다.
* 이러한 과정을 지나친다면, 앞으로 구현하게 될 함수가 훨씬 더 어렵게 느껴질 겁니다.
*/
// _.slice는 배열의 start 인덱스부터 end 인덱스 이전까지의 요소를 shallow copy하여 새로운 배열을 리턴합니다.
_.slice = function (arr, start, end) {
// 변수를 선언할 경우, 아래와 같이 콤마(,)를 이용해 선언할 수 있습니다.
// 이때, 콤마로 연결된 변수들은 모두 동일한 선언 키워드(let, const)가 적용됩니다.
// 이런 코딩 스타일도 가능하다는 것을 보여드리기 위한 예시일 뿐, 사용을 권장하는 것은 아닙니다.
// 오픈 소스에 기여하든, 회사 내에서 개발을 하든 본인이 속한 조직의 코딩 스타일, 코딩 컨벤션을 따르면 됩니다.
// 그리고 아래와 같은 코딩 스타일을 봐도 당황하지 않고 해석할 수 있으면 됩니다.
let _start = start || 0, // `start`가 undefined인 경우, slice는 0부터 동작합니다.
_end = end;
// 입력받은 인덱스가 음수일 경우, 마지막 인덱스부터 매칭한다. (예. -1 => arr.length - 1, -2 => arr.length - 2)
// 입력받은 인덱스는 0 이상이어야 한다.
if (start < 0) _start = Math.max(0, arr.length + start);
if (end < 0) _end = Math.max(0, arr.length + end);
// `end`가 생략될 경우(undefined), slice는 마지막 인덱스까지 동작합니다.
// `end`가 배열의 범위를 벗어날 경우, slice는 마지막 인덱스까지 동작합니다.
if (_end === undefined || _end > arr.length) _end = arr.length;
let result = [];
// `start`가 배열의 범위를 벗어날 경우, 빈 배열을 리턴합니다.
for (let i = _start; i < _end; i++) {
result.push(arr[i]);
}
return result;
};
// _.take는 배열의 처음 n개의 element를 담은 새로운 배열을 리턴합니다.
// n이 undefined이거나 음수인 경우, 빈 배열을 리턴합니다.
// n이 배열의 길이를 벗어날 경우, 전체 배열을 shallow copy한 새로운 배열을 리턴합니다.
_.take = function (arr, n) {
// TODO: 여기에 코드를 작성합니다.
};
// _.drop는 _.take와는 반대로, 처음 n개의 element를 제외한 새로운 배열을 리턴합니다.
// n이 undefined이거나 음수인 경우, 전체 배열을 shallow copy한 새로운 배열을 리턴합니다.
// n이 배열의 길이를 벗어날 경우, 빈 배열을 리턴합니다.
_.drop = function (arr, n) {
// TODO: 여기에 코드를 작성합니다.
};
// _.last는 배열의 마지막 n개의 element를 담은 새로운 배열을 리턴합니다.
// n이 undefined이거나 음수인 경우, 배열의 마지막 요소만을 담은 배열을 리턴합니다.
// n이 배열의 길이를 벗어날 경우, 전체 배열을 shallow copy한 새로운 배열을 리턴합니다.
// _.take와 _.drop 중 일부 또는 전부를 활용할 수 있습니다.
_.last = function (arr, n) {
// TODO: 여기에 코드를 작성합니다.
};
// _.each는 collection의 각 데이터에 반복적인 작업을 수행합니다.
// 1. collection(배열 혹은 객체)과 함수 iteratee(반복되는 작업)를 인자로 전달받아 (iteratee는 함수의 인자로 전달되는 함수이므로 callback 함수)
// 2. collection의 데이터(element 또는 property)를 순회하면서
// 3. iteratee에 각 데이터를 인자로 전달하여 실행합니다.
// iteratee에는 테스트 케이스에 따라서 다양한 함수가 할당됩니다.
// Array.prototype.forEach 메소드를 사용할 때, 다양한 형태의 callback 함수를 사용할 수 있었던 걸 기억하시나요?
// 우리가 만드는 _.each 함수도 그렇게 잘 작동하게 하기 위한 방법을 고민해 봅시다.
/*
* SpecRunner를 열고 each의 네 번째 테스트 케이스를 눌러 보시기 바랍니다.
* 이 테스트 케이스의 collection은 letters이고,
* iteratee는 익명함수 function(letter) { iterations.push(letter); }); 입니다.
*
* const letters = ['a', 'b', 'c'];
* const iterations = [];
* _.each(letters, function(letter) {
* iterations.push(letter);
* });
* expect(iterations).to.eql(['a', 'b', 'c']);
*
* iteratee는 차례대로 데이터(element 또는 value), 접근자(index 또는 key), collection을 다룰 수 있어야 합니다.
* 배열 arr을 입력받을 경우, iteratee(ele, idx, arr)
* 객체 obj를 입력받을 경우, iteratee(val, key, obj)
* 이처럼 collection의 모든 정보가 iteratee의 인자로 잘 전달되어야 모든 경우를 다룰 수 있습니다.
* 실제로 전달되는 callback 함수는 collection의 모든 정보가 필요하지 않을 수도 있습니다.
*/
// _.each는 명시적으로 어떤 값을 리턴하지 않습니다.
_.each = function (collection, iteratee) {
// TODO: 여기에 코드를 작성합니다.
};
// _.indexOf는 target으로 전달되는 값이 arr의 요소인 경우, 배열에서의 위치(index)를 리턴합니다.
// 그렇지 않은 경우, -1을 리턴합니다.
// target이 중복해서 존재하는 경우, 가장 낮은 index를 리턴합니다.
_.indexOf = function (arr, target) {
// 배열의 모든 요소에 접근하려면, 순회 알고리즘(iteration algorithm)을 구현해야 합니다.
// 반복문을 사용하는 것이 가장 일반적이지만, 지금부터는 이미 구현한 _.each 함수를 활용하여야 합니다.
// 아래 _.indexOf의 구현을 참고하시기 바랍니다.
let result = -1;
_.each(arr, function (item, index) {
if (item === target && result === -1) {
result = index;
}
});
return result;
};
// _.filter는 test 함수를 통과하는 모든 요소를 담은 새로운 배열을 리턴합니다.
// test(element)의 결과(return 값)가 truthy일 경우, 통과입니다.
// test 함수는 각 요소에 반복 적용됩니다.
_.filter = function (arr, test) {
// TODO: 여기에 코드를 작성합니다.
};
// _.reject는 _.filter와 정반대로 test 함수를 통과하지 않는 모든 요소를 담은 새로운 배열을 리턴합니다.
_.reject = function (arr, test) {
// TODO: 여기에 코드를 작성합니다.
};
// _.uniq는 주어진 배열의 요소가 중복되지 않도록 새로운 배열을 리턴합니다.
// 중복 여부의 판단은 엄격한 동치 연산(strict equality, ===)을 사용해야 합니다.
// 입력으로 전달되는 배열의 요소는 모두 primitive value라고 가정합니다.
_.uniq = function (arr) {
// TODO: 여기에 코드를 작성합니다.
};
// _.map은 iteratee(반복되는 작업)를 배열의 각 요소에 적용(apply)한 결과를 담은 새로운 배열을 리턴합니다.
// 함수의 이름에서 드러나듯이 _.map은 배열의 각 요소를 다른 것(iteratee의 결과)으로 매핑(mapping)합니다.
_.map = function (arr, iteratee) {
// TODO: 여기에 코드를 작성합니다.
// _.map 함수는 매우 자주 사용됩니다.
// _.each 함수와 비슷하게 동작하지만, 각 요소에 iteratee를 적용한 결과를 리턴합니다.
};
// _.pluck은
// 1. 객체 또는 배열을 요소로 갖는 배열과 각 요소에서 찾고자 하는 key 또는 index를 입력받아
// 2. 각 요소의 해당 값 또는 요소만을 추출하여 새로운 배열에 저장하고,
// 3. 최종적으로 새로운 배열을 리턴합니다.
// 예를 들어, 각 개인의 정보를 담은 객체를 요소로 갖는 배열을 통해서, 모든 개인의 나이만으로 구성된 별도의 배열을 만들 수 있습니다.
// 최종적으로 리턴되는 새로운 배열의 길이는 입력으로 전달되는 배열의 길이와 같아야 합니다.
// 따라서 찾고자 하는 key 또는 index를 가지고 있지 않은 요소의 경우, 추출 결과는 undefined 입니다.
_.pluck = function (arr, keyOrIdx) {
// _.pluck을 _.each를 사용해 구현하면 아래와 같습니다.
// let result = [];
// _.each(arr, function (item) {
// result.push(item[keyOrIdx]);
// });
// return result;
// _.pluck은 _.map을 사용해 구현하시기 바랍니다.
// TODO: 여기에 코드를 작성합니다.
};
// _.reduce는
// 1. 배열을 순회하며 각 요소에 iteratee 함수를 적용하고,
// 2. 그 결과값을 계속해서 누적(accumulate)합니다.
// 3. 최종적으로 누적된 결과값을 리턴합니다.
// 예를 들어, 배열 [1, 2, 3, 4]를 전부 더해서 10이라는 하나의 값을 리턴합니다.
// 각 요소가 처리될 때마다 누적되는 값은 차례대로 1, 1+2, 1+2+3, 1+2+3+4 입니다.
// 이처럼 _.reduce는 배열이라는 다수의 정보가 하나의 값으로 축소(응축, 환원, reduction)되기 때문에 reduce라는 이름이 붙게 된 것입니다.
// _.reduce는 위에서 구현한 많은 함수처럼, 입력으로 배열과 각 요소에 반복할 작업(iteratee)을 전달받습니다.
// iteratee에 대해서 복습하면 아래와 같습니다. (일반적으로 객체를 reduce 하지는 않으므로, 배열 부분만 복습합니다.)
// iteratee는 차례대로 데이터(element 또는 value), 접근자(index 또는 key), collection을 다룰 수 있어야 합니다.
// 배열 arr을 입력받을 경우, iteratee(ele, idx, arr)
// _.reduce는 반복해서 값을 누적하므로 이 누적되는 값을 관리해야 합니다.
// 따라서 _.reduce의 iteratee는 인자가 하나 더 추가되어 최종 형태는 아래와 같습니다.
// iteratee(acc, ele, idx, arr)
// 누적되는 값은 보통 tally, accumulator(앞글자만 따서 acc로 표기하기도 함)로 표현하거나
// 목적을 더 분명하게 하기 위해 sum(합), prod(곱), total 등으로 표현하기도 합니다.
// 이때, acc는 '이전 요소까지'의 반복 작업의 결과로 누적된 값입니다.
// ele는 잘 아시다시피 반복 작업을 수행할(아직 수행하지 않은) 현재의 요소입니다.
// 여기까지 내용을 정리하면 다음과 같습니다.
// _.reduce(arr, iteratee)
// iteratee(acc, ele, idx, arr)
// 그런데 사실 누적값에 대해서 빠뜨린 게 하나 있습니다.
// 바로 '누적값은 어디서부터 시작하는가'라는 의문에 대한 대답을 하지 않았습니다.
// 이를 해결하는 방법은 초기 값을 직접 설정하거나 자동으로 설정하는 것입니다.
// _.reduce는 세 번째 인자로 초기 값을 전달받을 수 있습니다.
// 이 세 번째 인자로 초기 값이 전달되는 경우, 그 값을 누적값의 기초(acc)로 하여 배열의 '첫 번째' 요소부터 반복 작업이 수행됩니다.
// 반면 초기 값이 전달되지 않은 경우, 배열의 첫 번째 요소를 누적값의 출발로 하여 배열의 '두 번째' 요소부터 반복 작업이 수행됩니다.
// 따라서 최종적인 형태는 아래와 같습니다.
// _.reduce(arr, iteratee, initVal)
// iteratee(acc, ele, idx, arr)
// 아래 예제를 참고하시기 바랍니다.
// const numbers = [1,2,3];
// const sum = _.reduce(numbers, function(total, number){
// return total + number;
// }); // 초기 값이 주어지지 않았으므로, 초기 값은 배열의 첫 요소인 1입니다. 두 번째 요소부터 반복 작업이 시작됩니다.
// // 1 + 2 = 3; (첫 작업의 결과가 누적되어 다음 작업으로 전달됩니다.)
// // 3 + 3 = 6; (마지막 작업이므로 최종적으로 6이 리턴됩니다.)
//
// const identity = _.reduce([3, 5], function(total, number){
// return total + number * number;
// }, 2); // 초기 값이 2로 주어졌습니다. 첫 번째 요소부터 반복 작업이 시작됩니다.
// // 2 + 3 * 3 = 11; (첫 작업의 결과가 누적되어 다음 작업으로 전달됩니다.)
// // 11 + 5 * 5 = 36; (마지막 작업이므로 최종적으로 36이 리턴됩니다.)
_.reduce = function (arr, iteratee, initVal) {
// TODO: 여기에 코드를 작성합니다.
};
01_bareMinimum.js 풀이
'use strict';
/**
* underbar 스프린트에 오신 것을 환영합니다!
*
* 여러분은 자바스크립트를 보다 효율적으로 사용하기 위해서 만들어진 underscore.js 라이브러리를
* 비슷하게 구현하면서 자바스크립트의 문법을 더욱 깊게 이해하는 시간을 가지게 됩니다.
*/
// _.identity는 전달인자(argument)가 무엇이든, 그대로 리턴합니다.
// 이 함수는 underbar의 기능 구현 및 테스트를 위해 재사용되는 함수입니다.
_.identity = function (val) {
// TODO: 여기에 코드를 작성합니다.
return val;
};
/**
* COLLECTIONS
* ===========
* collection은 영어로 '모음, 무리'라는 뜻입니다. 컴퓨터 과학에서는 '데이터(data, 자료)의 모음'으로 부를 수 있겠죠.
* 우리가 배웠던 collection의 종류는 무엇이 있을까요? 다양한 종류가 있지만 배열과 객체가 대표적입니다.
*
* 배열은 데이터들(요소, element)을 '순서대로' 모은 자료 구조입니다. 요소의 위치를 통해 데이터에 접근할 수 있습니다.
* 객체는 서로 관련있는 데이터들(속성, property)을 'key-value' 형태로 '순서 없이' 모은 자료 구조입니다. 속성에 부여된 키(key)를 통해 데이터에 접근할 수 있습니다.
*
* collection의 각 데이터를 다루는 것은 매우 흔한 작업 중에 하나입니다.
* 예를 들어, 아래의 작업들을 생각해 볼 수 있습니다.
* 배열의 요소 중 가장 큰 값을 찾는 것
* 배열의 모든 요소의 합을 구하는 것
* 객체에 특정 속성의 이름(key)이 존재하는지 확인하는 것
*
* 각 작업들은 collection의 각 데이터를 가지고 비슷한 처리를 합니다.
* 배열의 요소 중 가장 큰 값을 찾는 것 => 현재 데이터가 가장 큰 값인지 확인
* 배열의 모든 요소의 합을 구하는 것 => 현재 데이터를 누적값에 더하기
* 객체에 특정 속성의 이름(key)이 존재하는지 확인하는 것 => 현재 key가 원하는 값인지 확인하기
*
* 이처럼 비슷한 처리가 반복되기 때문에 이를 반복(iteration) 작업이라고 부를 수 있습니다.
* 자바스크립트는 이러한 반복(iteration) 작업을 위한 여러 수단들을 제공합니다.
* 반복문(for, for of, for in, while)과 반복을 위한 내장 메소드(arr.map, arr.filter)들이 대표적입니다.
* 사실 반복문의 제어 변수로 주로 사용하는 변수 i는 iteration의 앞 글자 i를 의미합니다.
* 아래의 반복문은 총 console.log가 총 세 번 반복됩니다.
* 이때, console.log와 같이 반복되는 작업을 iteratee(반복되는 것)라고 부를 수 있습니다.
* for (let i = 0; i < 3; i++) {
* console.log(i);
* }
*
* 이 반복문은 아래와 같이 해석됩니다.
* let i = 0;
* { // 0번째 반복
* console.log(0); // 변수 i가 0으로 치환됨
* i++; // i는 1이 됨
* }
*
* 1 < 3 이므로 다음 반복 작업을 실행
* { // 1번째 반복
* console.log(1); // 변수 i가 1로 치환됨
* i++; // i는 2이 됨
* }
*
* 2 < 3 이므로 다음 반복 작업을 실행
* { // 2번째 반복
* console.log(2); // 변수 i가 2로 치환됨
* i++; // i는 3이 됨
* }
*
* 3 < 3 이 아니므로 반복 작업 종료
*
*
*
*
* 이번 스프린트에서는 collection을 다루는 여러 함수를 직접 구현해 봅니다.
*
* IMPORTANT NOTE!
* ===========
* 아래에 _.slice가 이미 구현되어 있습니다. 이 함수를 가이드 삼아, 앞으로 나올 함수들을 구현해 보세요.
* 이번 스프린트에서 아래 예외를 제외하고 배열(Array), 집합(Set), 맵(Map)의 기본 메소드 사용은 금지되어 있습니다.
* 사용 가능한 내장 메소드: Array.prototype 의 'pop', 'push', 'shift', 'sort'
* 단, 새로운 함수를 구현할 때 이전에 구현한 함수를 활용해도 됩니다. (이미 해결한 문제를 또 해결할 필요는 없겠죠?)
* 사전에 이미 완료된 과제의 일부분을 만나게 될 경우, 반드시 코드를 잘 읽어 보고 이해하고 넘어가시기 바랍니다.
* 이러한 과정을 지나친다면, 앞으로 구현하게 될 함수가 훨씬 더 어렵게 느껴질 겁니다.
*/
// _.slice는 배열의 start 인덱스부터 end 인덱스 이전까지의 요소를 shallow copy하여 새로운 배열을 리턴합니다.
_.slice = function (arr, start, end) {
// 변수를 선언할 경우, 아래와 같이 콤마(,)를 이용해 선언할 수 있습니다.
// 이때, 콤마로 연결된 변수들은 모두 동일한 선언 키워드(let, const)가 적용됩니다.
// 이런 코딩 스타일도 가능하다는 것을 보여드리기 위한 예시일 뿐, 사용을 권장하는 것은 아닙니다.
// 오픈 소스에 기여하든, 회사 내에서 개발을 하든 본인이 속한 조직의 코딩 스타일, 코딩 컨벤션을 따르면 됩니다.
// 그리고 아래와 같은 코딩 스타일을 봐도 당황하지 않고 해석할 수 있으면 됩니다.
let _start = start || 0, // `start`가 undefined인 경우, slice는 0부터 동작합니다.
_end = end;
// 입력받은 인덱스가 음수일 경우, 마지막 인덱스부터 매칭한다. (예. -1 => arr.length - 1, -2 => arr.length - 2)
// 입력받은 인덱스는 0 이상이어야 한다.
if (start < 0) _start = Math.max(0, arr.length + start);
if (end < 0) _end = Math.max(0, arr.length + end);
// `end`가 생략될 경우(undefined), slice는 마지막 인덱스까지 동작합니다.
// `end`가 배열의 범위를 벗어날 경우, slice는 마지막 인덱스까지 동작합니다.
if (_end === undefined || _end > arr.length) _end = arr.length;
let result = [];
// `start`가 배열의 범위를 벗어날 경우, 빈 배열을 리턴합니다.
for (let i = _start; i < _end; i++) {
result.push(arr[i]);
}
return result;
};
// _.take는 배열의 처음 n개의 element를 담은 새로운 배열을 리턴합니다.
// n이 undefined이거나 음수인 경우, 빈 배열을 리턴합니다.
// n이 배열의 길이를 벗어날 경우, 전체 배열을 shallow copy한 새로운 배열을 리턴합니다.
_.take = function (arr, n) {
// TODO: 여기에 코드를 작성합니다.
let newArr = [];
if (n === undefined || n < 0) {
return [];
}
if (n >= arr.length) {
return arr;
}
for (let i = 0; i < n; i++) {
newArr.push(arr[i]);
}
return newArr;
};
// _.drop는 _.take와는 반대로, 처음 n개의 element를 제외한 새로운 배열을 리턴합니다.
// n이 undefined이거나 음수인 경우, 전체 배열을 shallow copy한 새로운 배열을 리턴합니다.
// n이 배열의 길이를 벗어날 경우, 빈 배열을 리턴합니다.
_.drop = function (arr, n) {
// TODO: 여기에 코드를 작성합니다.
let newArr = [];
if (n === undefined || n < 0) {
return arr;
}
if (n > arr.length) {
return [];
}
for (let i = n; i < arr.length; i++) {
newArr.push(arr[i]);
}
return newArr;
};
// _.last는 배열의 마지막 n개의 element를 담은 새로운 배열을 리턴합니다.
// n이 undefined이거나 음수인 경우, 배열의 마지막 요소만을 담은 배열을 리턴합니다.
// n이 배열의 길이를 벗어날 경우, 전체 배열을 shallow copy한 새로운 배열을 리턴합니다.
// _.take와 _.drop 중 일부 또는 전부를 활용할 수 있습니다.
_.last = function (arr, n) {
// TODO: 여기에 코드를 작성합니다.
if (n === 0) {
return [];
}
if (n === undefined || n < 0) {
return [arr[arr.length -1 ]];
}
if (n >= arr.length) {
return arr;
}
return _.drop(arr, arr.length - n);
};
// _.each는 collection의 각 데이터에 반복적인 작업을 수행합니다.
// 1. collection(배열 혹은 객체)과 함수 iteratee(반복되는 작업)를 인자로 전달받아 (iteratee는 함수의 인자로 전달되는 함수이므로 callback 함수)
// 2. collection의 데이터(element 또는 property)를 순회하면서
// 3. iteratee에 각 데이터를 인자로 전달하여 실행합니다.
// iteratee에는 테스트 케이스에 따라서 다양한 함수가 할당됩니다.
// Array.prototype.forEach 메소드를 사용할 때, 다양한 형태의 callback 함수를 사용할 수 있었던 걸 기억하시나요?
// 우리가 만드는 _.each 함수도 그렇게 잘 작동하게 하기 위한 방법을 고민해 봅시다.
/*
* SpecRunner를 열고 each의 네 번째 테스트 케이스를 눌러 보시기 바랍니다.
* 이 테스트 케이스의 collection은 letters이고,
* iteratee는 익명함수 function(letter) { iterations.push(letter); }); 입니다.
*
* const letters = ['a', 'b', 'c'];
* const iterations = [];
* _.each(letters, function(letter) {
* iterations.push(letter);
* });
* expect(iterations).to.eql(['a', 'b', 'c']);
*
* iteratee는 차례대로 데이터(element 또는 value), 접근자(index 또는 key), collection을 다룰 수 있어야 합니다.
* 배열 arr을 입력받을 경우, iteratee(ele, idx, arr)
* 객체 obj를 입력받을 경우, iteratee(val, key, obj)
* 이처럼 collection의 모든 정보가 iteratee의 인자로 잘 전달되어야 모든 경우를 다룰 수 있습니다.
* 실제로 전달되는 callback 함수는 collection의 모든 정보가 필요하지 않을 수도 있습니다.
*/
// _.each는 명시적으로 어떤 값을 리턴하지 않습니다.
_.each = function (collection, iteratee) {
// TODO: 여기에 코드를 작성합니다.
if (Array.isArray(collection)) {
for (let i = 0; i < collection.length; i++) {
iteratee(collection[i], i, collection);
}
} else if (typeof collection === 'object') {
// let arr = Object.keys(collection);
// for (let i = 0; i < arr.length; i++) {
// let key = arr[i];
// iteratee(collection[key], i, collection);
// }
for (let key in collection) {
iteratee(collection[key], key, collection);
}
}
};
// _.indexOf는 target으로 전달되는 값이 arr의 요소인 경우, 배열에서의 위치(index)를 리턴합니다.
// 그렇지 않은 경우, -1을 리턴합니다.
// target이 중복해서 존재하는 경우, 가장 낮은 index를 리턴합니다.
_.indexOf = function (arr, target) {
// 배열의 모든 요소에 접근하려면, 순회 알고리즘(iteration algorithm)을 구현해야 합니다.
// 반복문을 사용하는 것이 가장 일반적이지만, 지금부터는 이미 구현한 _.each 함수를 활용하여야 합니다.
// 아래 _.indexOf의 구현을 참고하시기 바랍니다.
let result = -1;
_.each(arr, function (item, index) {
if (item === target && result === -1) {
result = index;
}
});
return result;
};
// _.filter는 test 함수를 통과하는 모든 요소를 담은 새로운 배열을 리턴합니다.
// test(element)의 결과(return 값)가 truthy일 경우, 통과입니다.
// test 함수는 각 요소에 반복 적용됩니다.
_.filter = function (arr, test) {
// TODO: 여기에 코드를 작성합니다.
let result = [];
// for (let i = 0; i < arr.length; i++) {
// let currentEl = arr[i];
// if (test(currentEl)) {
// result.push(currentEl);
// }
// }
_.each(arr, function(element) {
if(test(element)) {
result.push(element);
}
});
return result;
};
// _.reject는 _.filter와 정반대로 test 함수를 통과하지 않는 모든 요소를 담은 새로운 배열을 리턴합니다.
_.reject = function (arr, test) {
// TODO: 여기에 코드를 작성합니다.
let result = [];
// for (let i = 0; i < arr.length; i++) {
// let currentEl = arr[i];
// if (!test(currentEl)) {
// result.push(currentEl);
// }
_.each(arr, function(currentEl) {
if(!test(currentEl)) result.push(currentEl)
});
return result;
};
// _.uniq는 주어진 배열의 요소가 중복되지 않도록 새로운 배열을 리턴합니다.
// 중복 여부의 판단은 엄격한 동치 연산(strict equality, ===)을 사용해야 합니다.
// 입력으로 전달되는 배열의 요소는 모두 primitive value라고 가정합니다.
// arr = [1, 2, 3, true, true, 'true', 2, 3, 'java']
// -> [1, 2, 3, true, 'true', 'java']
_.uniq = function (arr) {
// TODO: 여기에 코드를 작성합니다.
let result = [];
// for (let i = 0; i < arr.length; i++) {
// let currentEl = arr[i];
// if (result.indexOf(currentEl) === -1) {
// result.push(currentEl);
// }
// }
_.each(arr, function(currentEl) {
if(_.indexOf(result, currentEl) === -1) result.push(currentEl);
});
return result;
};
// _.map은 iteratee(반복되는 작업)를 배열의 각 요소에 적용(apply)한 결과를 담은 새로운 배열을 리턴합니다.
// 함수의 이름에서 드러나듯이 _.map은 배열의 각 요소를 다른 것(iteratee의 결과)으로 매핑(mapping)합니다.
_.map = function (arr, iteratee) {
// TODO: 여기에 코드를 작성합니다.
// _.map 함수는 매우 자주 사용됩니다.
// _.each 함수와 비슷하게 동작하지만, 각 요소에 iteratee를 적용한 결과를 리턴합니다.
let result = [];
// for (let i = 0; i < arr.length; i++) {
// let currentEl = arr[i];
// result.push(iteratee(currentEl));
// }
_.each(arr, function(currentEl) {
result.push(iteratee(currentEl));
})
return result;
};
// _.pluck은
// 1. 객체 또는 배열을 요소로 갖는 배열과 각 요소에서 찾고자 하는 key 또는 index를 입력받아
// 2. 각 요소의 해당 값 또는 요소만을 추출하여 새로운 배열에 저장하고,
// 3. 최종적으로 새로운 배열을 리턴합니다.
// 예를 들어, 각 개인의 정보를 담은 객체를 요소로 갖는 배열을 통해서, 모든 개인의 나이만으로 구성된 별도의 배열을 만들 수 있습니다.
// 최종적으로 리턴되는 새로운 배열의 길이는 입력으로 전달되는 배열의 길이와 같아야 합니다.
// 따라서 찾고자 하는 key 또는 index를 가지고 있지 않은 요소의 경우, 추출 결과는 undefined 입니다.
_.pluck = function (arr, keyOrIdx) {
// _.pluck을 _.each를 사용해 구현하면 아래와 같습니다.
// let result = [];
// _.each(arr, function (item) {
// result.push(item[keyOrIdx]);
// });
// return result;
// _.pluck은 _.map을 사용해 구현하시기 바랍니다.
// TODO: 여기에 코드를 작성합니다.
// let result = [];
// for (let i = 0; i < arr.length; i++) {
// result.push(arr[i][keyOrIdx]);
// }
// let result = _.map(arr, function(currentEl) {
// return currentEl[keyOrIdx];
// })
let result = _.map(arr, (currentEl => currentEl[keyOrIdx]));
return result;
};
// _.reduce는
// 1. 배열을 순회하며 각 요소에 iteratee 함수를 적용하고,
// 2. 그 결과값을 계속해서 누적(accumulate)합니다.
// 3. 최종적으로 누적된 결과값을 리턴합니다.
// 예를 들어, 배열 [1, 2, 3, 4]를 전부 더해서 10이라는 하나의 값을 리턴합니다.
// 각 요소가 처리될 때마다 누적되는 값은 차례대로 1, 1+2, 1+2+3, 1+2+3+4 입니다.
// 이처럼 _.reduce는 배열이라는 다수의 정보가 하나의 값으로 축소(응축, 환원, reduction)되기 때문에 reduce라는 이름이 붙게 된 것입니다.
// _.reduce는 위에서 구현한 많은 함수처럼, 입력으로 배열과 각 요소에 반복할 작업(iteratee)을 전달받습니다.
// iteratee에 대해서 복습하면 아래와 같습니다. (일반적으로 객체를 reduce 하지는 않으므로, 배열 부분만 복습합니다.)
// iteratee는 차례대로 데이터(element 또는 value), 접근자(index 또는 key), collection을 다룰 수 있어야 합니다.
// 배열 arr을 입력받을 경우, iteratee(ele, idx, arr)
// _.reduce는 반복해서 값을 누적하므로 이 누적되는 값을 관리해야 합니다.
// 따라서 _.reduce의 iteratee는 인자가 하나 더 추가되어 최종 형태는 아래와 같습니다.
// iteratee(acc, ele, idx, arr)
// 누적되는 값은 보통 tally, accumulator(앞글자만 따서 acc로 표기하기도 함)로 표현하거나
// 목적을 더 분명하게 하기 위해 sum(합), prod(곱), total 등으로 표현하기도 합니다.
// 이때, acc는 '이전 요소까지'의 반복 작업의 결과로 누적된 값입니다.
// ele는 잘 아시다시피 반복 작업을 수행할(아직 수행하지 않은) 현재의 요소입니다.
// 여기까지 내용을 정리하면 다음과 같습니다.
// _.reduce(arr, iteratee)
// iteratee(acc, ele, idx, arr)
// 그런데 사실 누적값에 대해서 빠뜨린 게 하나 있습니다.
// 바로 '누적값은 어디서부터 시작하는가'라는 의문에 대한 대답을 하지 않았습니다.
// 이를 해결하는 방법은 초기 값을 직접 설정하거나 자동으로 설정하는 것입니다.
// _.reduce는 세 번째 인자로 초기 값을 전달받을 수 있습니다.
// 이 세 번째 인자로 초기 값이 전달되는 경우, 그 값을 누적값의 기초(acc)로 하여 배열의 '첫 번째' 요소부터 반복 작업이 수행됩니다.
// 반면 초기 값이 전달되지 않은 경우, 배열의 첫 번째 요소를 누적값의 출발로 하여 배열의 '두 번째' 요소부터 반복 작업이 수행됩니다.
// 따라서 최종적인 형태는 아래와 같습니다.
// _.reduce(arr, iteratee, initVal)
// iteratee(acc, ele, idx, arr)
// 아래 예제를 참고하시기 바랍니다.
// const numbers = [1,2,3];
// const sum = _.reduce(numbers, function(total, number){
// return total + number;
// }); // 초기 값이 주어지지 않았으므로, 초기 값은 배열의 첫 요소인 1입니다. 두 번째 요소부터 반복 작업이 시작됩니다.
// // 1 + 2 = 3; (첫 작업의 결과가 누적되어 다음 작업으로 전달됩니다.)
// // 3 + 3 = 6; (마지막 작업이므로 최종적으로 6이 리턴됩니다.)
//
// const identity = _.reduce([3, 5], function(total, number){
// return total + number * number;
// }, 2); // 초기 값이 2로 주어졌습니다. 첫 번째 요소부터 반복 작업이 시작됩니다.
// // 2 + 3 * 3 = 11; (첫 작업의 결과가 누적되어 다음 작업으로 전달됩니다.)
// // 11 + 5 * 5 = 36; (마지막 작업이므로 최종적으로 36이 리턴됩니다.)
_.reduce = function (arr, iteratee, initVal) {
// TODO: 여기에 코드를 작성합니다.
let acc = initVal;
// for (let i = 0; i < arr.length; i++) {
// if (acc === undefined) {
// acc = arr[i];
// } else {
// acc = iteratee(acc, arr[i], i, arr);
// }
// }
_.each(arr, function(currentEl, idx, src) {
if(acc === undefined) acc = currentEl;
else acc = iteratee(acc, currentEl, idx, src);
})
return acc;
};