javascript 26강 배열 및 고차함수 2


JavaScript 26강 배열 고차 함수

  • 26강 :

    • 배열 개요 및 배열 메소드

    • 배열 고차 함수

      • Array.prototype.sort()
      • Array.prototype.forEach
      • Array.prototype.map
      • Array.prototype.filter
      • Array.prototype.reduce
      • Array.prototype.some
      • Array.prototype.every
      • Array.prototype.find
      • Array.prototype.findIndex


26강 - 2

26강 - 1 내용 : 배열 개요 및 배열 메소드

배열 고차함수

고차 함수(High Order Function, HOF)는 함수를 인자로 전달받거나 함수를 반환하는 함수를 말한다.
다시 말해, 고차 함수는 인자로 받은 함수를 필요한 시점에 호출하거나 클로저를 생성하여 반환한다.

반복문,조건문은 가독성을 해치고, 변수의 값에 대한 보호가 안되기 때문에 오류 발생의 원이 될 수 있다.

함수형 프로그래밍은 순수 함수를 통해 부수 효과를 최대한 억제하여 오류를 피하고 프로그램의 안정성을 높이려는 노력의 일환으로 고차함수를 사용한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 고차 함수 makeCounter는 함수를 인수로 전달받고 클로저를 반환한다.
function makeCounter(predicate) {
// 자유 변수. num의 상태는 유지되어야 한다.
let num = 0;
// num의 상태를 유지하는 클로저를 반환한다.
return function () {
// predicate를 호출하여 자유 변수 num의 상태를 변화시킨다.
num = predicate(num);
return num;
};
}

// 보조 함수
function increase(n) {
return ++n;
}

// 보조 함수
function decrease(n) {
return --n;
}

// 고차 함수 makeCounter는 함수를 인수로 전달받고 클로저를 반환한다.
const increaser = makeCounter(increase);
console.log(increaser()); // 1
console.log(increaser()); // 2

// 고차 함수 makeCounter는 함수를 인수로 전달받고 클로저를 반환한다.
const decreaser = makeCounter(decrease);
console.log(decreaser()); // -1
console.log(decreaser()); // -2

Array.prototype.sort()

  • 배열의 요소를 적절하게 정렬한다.
  • 원본 배열을 적접 변경하여 정렬된 배열을 반환한다.
  • 오름차순으로 정렬한다. 내림차순은 reverse 메소를 사용한다.
  • sort 메소드는 Unicode 순서(문자열)로 정렬을 하기 떄문에 숫자는 배치를 해주지 않는다.
1
2
3
4
5
6
7
const fruits = ['Banana', 'Orange', 'Apple'];

// 오름차순(ascending) 정렬
fruits.sort();

// sort 메소드는 원본 배열을 직접 변경한다.
console.log(fruits); // ['Apple', 'Banana', 'Orange']

위 예제처럼 문자열은 문제 없이 오름차순 정렬이 된다.

1
2
3
4
5
6
const points = [40, 100, 1, 5, 2, 25, 10];

points.sort();

// 숫자 요소들로 이루어진 배열은 의도한 대로 정렬되지 않는다.
console.log(points); // [1, 10, 100, 2, 25, 40, 5]

하지만 숫자로 된 배열을 sort 시에는 문제가 생긴다.

따라서 아래와 같은 코드를 이용한다.

배열명.sort(function(a,b){return a - b;}); : 숫자 정렬시 사용. 오름차순으로 정렬한다.
배열명.sort(function(a,b){return b - a;}) : 숫자 정렬 시 사용. 내림차순으로 정렬한다.

1
2
3
4
5
6
7
8
9
const points = [40, 100, 1, 5, 2, 25, 10];

// 비교 함수의 반환값이 0보다 작은 경우, a를 우선하여 정렬한다. 오름차순 정렬
points.sort(function (a, b) { return a - b; });
console.log(points); // [1, 2, 5, 10, 25, 40, 100]

// 비교 함수의 반환값이 0보다 큰 경우, b를 우선하여 정렬한다. 내림차순 정렬
points.sort(function (a, b) { return b - a; });
console.log(points); // [100, 40, 25, 10, 5, 2, 1]

객체를 요소로 갖는 배열을 정렬하는 예제는 아래와 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
const todos = [
{ id: 4, content: 'JavaScript' },
{ id: 1, content: 'HTML' },
{ id: 2, content: 'CSS' }
];

// 비교 함수
function compare(key) {
return function (a, b) {
// 프로퍼티 값이 문자열인 경우, - 산술 연산으로 비교하면 NaN이 나오므로 비교 연산을 사용한다.
return a[key] > b[key] ? 1 : (a[key] < b[key] ? -1 : 0);
};
}

// id를 기준으로 정렬
todos.sort(compare('id'));
console.log(todos);
/*
[
{ id: 1, content: 'HTML' },
{ id: 2, content: 'CSS' },
{ id: 4, content: 'JavaScript' }
]
*/

// content를 기준으로 정렬
todos.sort(compare('content'));
console.log(todos);
/*
[
{ id: 2, content: 'CSS' },
{ id: 1, content: 'HTML' },
{ id: 4, content: 'JavaScript' }
]
*/

Array.prototype.forEach

  • forEach 메소드는 for 문을 대체할 수 있는 메소드이다.
  • forEach의 반환값은 undefined다.
  • 희소 배열의 존재하지 않는 요소는 순회 대상에서 제외된다.
  • 원본 배열을 변경할 수 없다. (단, 콜백함수가 변경할수는 있다.)
  • 배열을 순회하며 배열의 각 요소에 대하여 인자로 주어진 콜백함수를 실행한다.
  • 배열의 모든 요소를 빠짐없이 모두 순회하며 중간에 순회를 중단할 수 없다.(즉, break / continue 사용 불가)
  • this를 전달 받을 수 있다.
1
2
3
4
5
6
7
8
9
// forEach 메소드는 전달받은 콜백 함수를 호출하면서 3개(요소값, 인덱스, this)의 인수를 전달한다.
[1, 2, 3].forEach((item, index, self) => {
console.log(`요소값: ${item}, 인덱스: ${index}, this: ${self}`);
});
/*
요소값: 1, 인덱스: 0, this: 1,2,3
요소값: 2, 인덱스: 1, this: 1,2,3
요소값: 3, 인덱스: 2, this: 1,2,3
*/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const numbers = [1, 2, 3];
let pows = [];

// for 문으로 순회
for (let i = 0; i < numbers.length; i++) {
pows.push(numbers[i] ** 2);
}

console.log(pows); // [1, 4, 9]

pows = [];

// forEach 메소드로 순회
numbers.forEach(item => pows.push(item ** 2));

console.log(pows); // [1, 4, 9]

numbers에 있는 각 요소를 item이라 하고 item 제곱화하여 pows에 각각 담는다.


forEach 메소드는 원본 배열(this)을 변경하지 않는다. 하지만 콜백 함수가 원본 배열(this)을 변경할 수는 있다.

1
2
3
4
5
const numbers = [1, 2, 3];
// 원본 배열을 직접 변경하려면 콜백 함수의 3번째 인자(this)를 사용한다.
numbers.forEach((item, index, self) => self[index] = Math.pow(item, 2));

console.log(numbers); // [1, 4, 9]

forEach 메소드에 2번째 인자로 forEach 메소드 내부에서 this로 사용될 객체를 전달할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Numbers {
numberArray = [];

errorMultiply(arr) {
arr.forEach(function (item) {
// 일반 함수로 호출되는 콜백 함수 내부의 this는 전역 객체를 가리킨다.
// TypeError: Cannot read property 'numberArray' of undefined
this.numberArray.push(item * item);
});
}

multiply(arr) {
arr.forEach(function (item) {
// 외부에서 this를 전달하지 않으면 this는 전역 객체를 가리킨다.
this.numberArray.push(item * item);
}, this); // forEach 메소드 내부에서 this로 사용될 객체를 전달
}
}

const numbers = new Numbers();
numbers.multiply([1, 2, 3]);
console.log(numbers.numberArray); // [1 , 4 , 9]

더 손쉽게는 ES6의 화살표 함수를 이용한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
class Numbers {
numberArray = [];

multiply(arr) {
// 화살표 함수 내부에서 this를 참조하면
// 상위 컨텍스트, 즉 multiply 메소드 내부의 this를 그대로 참조한다.
arr.forEach(item => this.numberArray.push(item * item));
}
}

const numbers = new Numbers();
numbers.multiply([1, 2, 3]);
console.log(numbers.numberArray); // [1, 4, 9]

Array.prototype.map

  • 배열을 순회하며 배열의 각 요소에 대하여 인자로 주어진 콜백 함수를 실행한다.
  • 콜백 함수의 반환한 값들이 요소로서 추가된 새로운 배열을 반환한다.
  • 원본 배열은 변경되지 않는다.
  • 희소 배열의 존재하지 않는 요소는 순회 대상에서 제외된다.
  • this를 전달 받을 수 있다.
1
2
3
4
5
6
7
8
9
10
// map 메소드는 전달받은 콜백 함수를 호출하면서 3개(요소값, 인덱스, this)의 인수를 전달한다.
[1, 2, 3].map((item, index, self) => {
console.log(`요소값: ${item}, 인덱스: ${index}, this: ${self}`);
return item;
});
/*
요소값: 1, 인덱스: 0, this: 1,2,3
요소값: 2, 인덱스: 1, this: 1,2,3
요소값: 3, 인덱스: 2, this: 1,2,3
*/
1
2
3
4
5
6
7
8
9
10
11
12
13
const numbers = [1, 4, 9];

// 배열을 순회하며 배열의 각 요소에 대하여 인자로 주어진 콜백 함수를 실행한다.
// 그리고 콜백 함수의 반환한 값들이 요소로서 추가된 새로운 배열을 반환한다.
const roots = numbers.map(item => Math.sqrt(item));

// 위 코드의 축약표현은 아래와 같다.
// const roots = numbers.map(Math.sqrt);

// map 메소드는 새로운 배열을 반환한다
console.log(roots); // [ 1, 2, 3 ]
// map 메소드는 원본 배열은 변경하지 않는다
console.log(numbers); // [ 1, 4, 9 ]

새로운 배열의 length는 map 메소드를 호출한 배열, 즉 this의 length와 반드시 일치한다.

forEach 메소드와 마찬가지로 map 메소드에 두번째 인자로 map 메소드 내부에서 this로 사용될 객체를 전달할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Prefixer {
constructor(prefix) {
this.prefix = prefix;
}

prefixArray(arr) {
return arr.map(function (item) {
// 외부에서 this를 전달하지 않으면 this는 전역 객체를 가리킨다.
return this.prefix + item;
}, this); // map 메소드 내부에서 this로 사용될 객체를 전달
}
}

const pre = new Prefixer('-webkit-');
const preArr = pre.prefixArray(['linear-gradient', 'border-radius']);
console.log(preArr);
// ['-webkit-linear-gradient', '-webkit-border-radius']

보다 나은 방법은 ES6의 화살표 함수를 사용하는 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Prefixer {
constructor(prefix) {
this.prefix = prefix;
}

prefixArray(arr) {
// 화살표 함수 내부에서 this를 참조하면
// 상위 컨텍스트, 즉 multiply 메소드의 this를 그대로 참조한다.
return arr.map(item => this.prefix + item);
}
}

const pre = new Prefixer('-webkit-');
const preArr = pre.prefixArray(['linear-gradient', 'border-radius']);
console.log(preArr);
// ['-webkit-linear-gradient', '-webkit-border-radius']

Array.prototype.filter

  • 배열을 순회하며 배열의 각 요소에 대하여 인자로 주어진 콜백 함수를 실행한다.
  • 실행 결과가 true인 배열 요소의 값만을 추출한 새로운 배열을 반환
  • 원본 배열은 변경되지 않는다.
  • this를 전달 받을 수 있다.
1
2
3
4
5
6
7
8
9
10
// filter 메소드는 전달받은 콜백 함수를 호출하면서 3개(요소값, 인덱스, this)의 인수를 전달한다.
[1, 2, 3].filter((item, index, self) => {
console.log(`요소값: ${item}, 인덱스: ${index}, this: ${self}`);
return item % 2;
});
/*
요소값: 1, 인덱스: 0, this: 1,2,3
요소값: 2, 인덱스: 1, this: 1,2,3
요소값: 3, 인덱스: 2, this: 1,2,3
*/
1
2
3
4
5
6
const numbers = [1, 2, 3, 4, 5];

// 홀수만을 필터링한다 (1은 true로 평가된다)
const odds = numbers.filter(item => item % 2);

console.log(odds); // [1, 3, 5]

Array.prototype.reduce

  • reduce 메소드는 배열을 순회하며 콜백 함수의 이전 반환값과 배열의 각 요소에 대하여 인자로 주어진 콜백 함수를 실행하여 하나의 결과값을 반환한다.
  • 원본 배열은 변경되지 않는다.
  • 첫번째 인수로 콜백 함수 [ 반환값, 요소값, 인덱스, reduce호출한 배열(this)]
  • 두번째 인수는 초기값.
  • reduce 메소드는 배열을 순회하며 단일값을 구해야 하는 경우에 사용한다.
1
2
3
4
// 1부터 4까지 누적을 구한다.
const sum = [1, 2, 3, 4].reduce((pre, cur, index, self) => pre + cur, 0);

console.log(sum); // 10

첫번째 인수로 전달받은 콜백 함수는 4개의 인수를 전달받아 배열의 length만큼 총 4회 호출된다.

이때 콜백 함수로 전달되는 인수와 반환값은 아래와 같다.

구분 콜백 함수에 전달된 인수 콜백 함수의 반환값
pre cur index self
첫번째 순회 0 (초기값) 1 0 [1, 2, 3, 4] 1 (pre + cur)
두번째 순회 1 2 1 [1, 2, 3, 4] 3 (pre + cur)
세번째 순회 3 3 2 [1, 2, 3, 4] 6 (pre + cur)
네번째 순회 6 4 3 [1, 2, 3, 4] 10 (pre + cur

  • 평균 구하기
1
2
3
4
5
6
7
8
9
10
11
12
const values = [1, 2, 3, 4, 5, 6];

const average = values.reduce((pre, cur, i, self) => {
// 마지막 순회인 경우, 누적값으로 평균을 구해 반환
if (i === self.length - 1) {
return (pre + cur) / self.length;
}
// 마지막 순회가 아닌 경우, 누적값을 반환
return pre + cur;
});

console.log(average); // 3
  • 최대값 구하기
1
2
3
4
5
6
7
8
9
10
const values = [1, 2, 3, 4, 5];

const max = values.reduce((pre, cur) => (pre > cur ? pre : cur), 0);
console.log(max); // 5

하지만 Math.max 메소드를 사용하는 방법이 보다 직관적이다.
const values = [1, 2, 3, 4, 5];

const max = Math.max(...values);
console.log(max); // 5
  • 중복된 요소의 개수 구하기
1
2
3
4
5
6
7
8
9
10
11
const fruits = ['banana', 'apple', 'orange', 'orange', 'apple'];

const count = fruits.reduce((pre, cur) => {
// 첫번째 순회: pre => {}, cur => 'banana'
// 빈 객체에 요소값을 프로퍼티 키로 추가하고 프로퍼티 값을 할당
// 만약 프로퍼티 값이 undefined이면 0으로 초기화
pre[cur] = (pre[cur] || 0) + 1;
return pre;
}, {});

console.log(count); // { banana: 1, apple: 2, orange: 2 }
  • 중첩 배열 평탄화
1
2
3
4
5
6
const values = [1, [2, 3], 4, [5, 6]];

const flatten = values.reduce((pre, cur) => pre.concat(cur), []);
// [1] => [1, 2, 3] => [1, 2, 3, 4] => [1, 2, 3, 4, 5, 6]

console.log(flatten); // [1, 2, 3, 4, 5, 6]
  • 중복 요소 제거 ( reduce보다는 filter이용하는게 더 직관적이다. )
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const values = [1, 2, 1, 3, 5, 4, 5, 3, 4, 4];

const result = values.reduce((pre, cur, i, self) => {
// 순회중인 요소의 인덱스가 자신의 인덱스라면 처음 순회하는 요소이다.
// 이 요소만 배열에 담아 반환한다.
// 순회중인 요소의 인덱스가 자신의 인덱스가 아니라면 중복된 요소이다.
// 3번째 순회: [1, 2], 1, 2, [1, 2, 1, 3, 5, 4, 5, 3, 4, 4]
// if ([1, 2, 1, 3, 5, 4, 5, 3, 4, 4].indexOf(1) === 2) => if(0 === 2)
if (self.indexOf(cur) === i) pre.push(cur);
return pre;
}, []);

console.log(result); // [1, 2, 3, 5, 4]

// filter 사용 시
const values2 = [1, 2, 1, 3, 5, 4, 5, 3, 4, 4];

// 순회중인 요소의 인덱스가 자신의 인덱스라면 처음 순회하는 요소이다. 이 요소만 반환한다.
const result = values2.filter((v, i, self) => self.indexOf(v) === i);
console.log(result); // [1, 2, 3, 5, 4]

이처럼 map, filter, some, every, find와 같은 모든 배열 고차 함수는 reduce로 구현할 수 있다.

1
2
const sum = [].reduce((pre, cur) => pre + cur);
// TypeError: Reduce of empty array with no initial value

이처럼 빈 배열로 reduce 메소드를 호출하면 에러가 발생한다.
reduce 메소드에 초기값을 전달하면 에러가 발생하지 않는다.

1
2
const sum = [].reduce((pre, cur) => pre + cur, 0);
console.log(sum); // 0
  • 객체의 프로퍼티 값을 합산하는 경우에는 반드시 초기값을 전달해야 한다.
1
2
3
4
5
6
7
8
9
10
11
12
const products = [
{ id: 1, price: 100 },
{ id: 2, price: 200 },
{ id: 3, price: 300 }
];

// 1번째 순회 : pre => 0, cur => 100
// 2번째 순회 : pre => 100, cur => 200
// 3번째 순회 : pre => 300, cur => 300
const priceSum = products.reduce((pre, cur) => pre + cur.price, 0);

console.log(priceSum); // 600

Array.prototype.some

  • 배열을 순회하며 요소 중 하나라도 콜백 함수의 테스트를 통과하면 true, 모든 요소가 콜백 함수의 테스트를 통과하지 못하면 false를 반환한다.
  • 2번째 인자로 this를 전달 받을 수 있다.
1
2
3
4
5
6
7
8
9
10
11
// 배열의 요소 중에 10보다 큰 요소가 1개 이상 존재하는지 확인
let result = [5, 10, 15].some(item => item > 10);
console.log(result); // true

// 배열의 요소 중에 0보다 작은 요소가 1개 이상 존재하는지 확인
result = [5, 10, 15].some(item => item < 0);
console.log(result); // false

// 배열의 요소 중에 'banana'가 1개 이상 존재하는지 확인
result = ['apple', 'banana', 'mango'].some(item => item === 'banana');
console.log(result); // true

Array.prototype.every

  • every 메소드는 배열을 순회하며 모든 요소가 콜백 함수의 테스트를 통과하면 true, 요소 중 하나라도 콜백 함수의 테스트를 통과하지 못하면 false를 반환한다.
  • forEach, map, filter 메소드와 마찬가지로 every 메소드의 콜백 함수는 요소값, 인덱스, 메소드를 호출한 배열, 즉 this를 전달 받을 수 있다.
  • 2번째 인자로 this를 전달 받을 수 있다.
1
2
3
4
5
6
7
// 배열의 모든 요소가 3보다 큰지 확인
let result = [5, 10, 15].every(item => item > 3);
console.log(result); // true

// 배열의 모든 요소가 10보다 큰지 확인
result = [5, 10, 15].every(item => item > 10);
console.log(result); // false

Array.prototype.find

  • 배열을 순회하며 각 요소에 대하여 인자로 주어진 콜백 함수를 실행하여 그 결과가 참인 첫번째 요소를 반환한다.
  • 참인 요소가 존재하지 않는다면 undefined를 반환한다.
  • 반드시 첫번쨰 요소만 반환하고 끝난다.
  • 2번째 인자로 this를 전달 받을 수 있다.

아래 예제에서는 프로퍼티 id의 값이 2인 배열 요소가 2개 있다. 그러나 정작 반환하는 것은 name: ‘Kim’ 뿐이다.

1
2
3
4
5
6
7
8
9
10
11
12
const users = [
{ id: 1, name: 'Lee' },
{ id: 2, name: 'Kim' },
{ id: 2, name: 'Choi' },
{ id: 3, name: 'Park' }
];

// id가 2인 요소를 반환한다.
const result = users.find(item => item.id === 2);

// Array#find는 배열이 아니라 요소를 반환한다.
console.log(result); // {id: 2, name: 'Kim'}

Array.prototype.findIndex

  • 배열을 순회하며 각 요소에 대하여 인자로 주어진 콜백 함수를 실행하여 그 결과가 참인 첫번째 요소의 인덱스를 반환한다.
  • 콜백 함수의 실행 결과가 참인 요소가 존재하지 않는다면 -1를 반환한다.
  • 반드시 첫번쨰 요소의 인덱스만 반환하고 끝난다.
  • 2번째 인자로 this를 전달 받을 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const users = [
{ id: 1, name: 'Lee' },
{ id: 2, name: 'Kim' },
{ id: 2, name: 'Choi' },
{ id: 3, name: 'Park' }
];

function predicate(key, value) {
// key와 value를 기억하는 클로저를 반환
return item => item[key] === value;
}

// Array#findIndex는 콜백 함수를 실행하여 그 결과가 참인 첫번째 요소의 인덱스를 반환한다.
// id가 2인 요소의 인덱스를 구한다.
let index = users.findIndex(predicate('id', 2));
console.log(index); // 1

// name이 'Park'인 요소의 인덱스를 구한다.
index = users.findIndex(predicate('name', 'Park'));
console.log(index); // 3

Share