[자바스크립트]모던 자바스크립트 Deep Dive - 27. 배열

June·2021년 10월 30일
0

자바스크립트 배열은 배열이 아니다

자바스크립트 배열은 일반적인 자료 구조와 달리, 메모리 공간이 동일한 크기를 갖지 않아도 되며, 연속적으로 이어져 있지 않을 수도 있다. 배열의 요소가 연속적으로 이어져 있지 않는 배열을 희소 배열이라고 한다. 자바스크립트의 배열은 일반적인 배열의 동작을 흉내낸 특수한 객체다.

자바스크립트 배열은 인덱스를 나타내는 문자열을 프로퍼티 키로 가지며, length 프로퍼티를 갖는 특수한 객체다. 자바스크립트 배열의 요소는 사실 프로퍼티 값이다. 자바스크립트에서 사용할 수 있는 모든 값은 객체의 프로퍼티 값이 될 수 있으므로 어떤 타입의 값이라도 배열의 요소가 될 수 있다.

자바스크립트 배열은 해시 테이블로 구현된 객체이므로 인덱스로 요소에 접근하는 일반적인 배열봗 성능적인 면에서 느릴 수밖에 없는 구조적인 단점이 있다. 하지만 특정 요소를 검색하거나 삽입 또는 삭제 하는 경우에는 일반적인 배열보다 빠른 성능을 기대할 수 있다.

length 프로퍼티와 희소 배열

length 프로퍼티 값은 요소의 개수, 즉 배열의 길이를 바탕으로 결정되지만 임의의 숫자 값을 명시적으로 할당할 수도 있다.

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

arr.length = 3;

// 배열의 길이가 5에서 3으로 줄어든다.
console.log(arr); // [1,2,3]

주의할 것은 현재 length 프로퍼티 값보다 큰 숫자 값을 할당하는 경우다. 이때 length 프로퍼티 값은 변경되지만 실제 배열의 길이가 늘어나지는 않는다.

const arr = [1];

arr.length = 3;
// length 프로퍼티 값은 변경되지만 실제로 배열의 길이가 늘어나지 않는다.
console.log(arr.length); // 3
console.log(arr); [1, empty*2]

실제 배열에는 아무런 변함이 없다. 값 없이 비어있는 요소를 위해 메모리 공간을 확보하지 않으며 빈 요소를 생성하지도 않는다.

이처럼 배열의 요소가 연속적으로 위치하지 않고 일부가 비어 있는 배열을 희소 배열이라고 한다. 중간이나 앞부분이 비어 있을 수도 있다.

희소 배열은 length와 배열 요소의 개수가 일치하지 않는다. 희소 배열은 사용하지 않는 것이 좋다.

배열 생성

배열 리터럴

const arr = [1,2,3];
const sparse = [1, , 3] // 희소 배열

Array 생성자 함수

  • 전달된 인수가 1개이고 숫자인 경우 length 프로퍼티 값이 인수인 배열을 생성
    . 이때 생성된 배열은 희소 배열이다.
const arr = new Array(10);
console.log(arr); // [empty * 10]
console.log(arr.length); // 10
  • 전달된 인수가 2개 이상이거나 숫자가 아닌 경우 인수를 요소로 갖는 배열 생성
new Array(1,2,3); // [1,2,3]
new Array({}); // [{}]

Array.of

ES6에서 도입된 Array.of 메서드는 전달된 인수를 요소로 갖는 배열을 생성한다.

Array.of(1); // [1]
Array.of(1,2,3); // [1,2,3]
Array.of('string'); // ['string']

Array.from

Es6에서 도입된 Array.from 메서드는 유사 배열 객체 또는 이터러블 객체를 인수로 전달받아 배열로 변환하여 반환한다.

Array.from('Hello'); // ['H', 'e', 'l', 'l', 'o']

Array.from을 사용하면 두 번째 인수로 전달된 콜백 함수를 통해 값을 만들면서 요소를 채울 수 있다. Array.from 메서드는 두 번째 인수로 전달한 콜백 함수에 첫 번째 인수에 의해 생성된 배열의 요소값과 인덱스를 순차적으로 전달하면서 호출하고, 콜백 함수의 반환값으로 구성된 배열을 반환한다.

// Array.from에 length만 존재하는 유사 배열 객체를 전달하면 undefined를 요소로 채운다.
Array.from({length: 3}); // [undefined, undefined, undefined]

// Array.from은 두 번째 인수로 전달한 콜백 함수의 반환값으로 구성된 배열을 반환한다.
Array.from({length: 3}), (_, i) => i); // [0, 1, 2]

유사 배열 객체는 마치 배열처럼 인덱스로 프로퍼티 값에 접근할 수 있고, length 프로퍼티를 갖는 객체를 말하낟. 이터러블 객체는 Symbol.iterator 메서드를 구현하여 for...of문으로 순회할 수 있으며, 스프레드 문법과 배열 디스트럭처링 할당의 대상으로 사용할 수 있는 객체를 말한다.

배열 요소의 참조

배열은 사실 인덱스를 나타내는 문자열을 프로퍼티 키로 갖는 객체다. 따라서 존재하지 않는 프로퍼티 키로 객체의 프로퍼티에 접근했을 때 undefined를 반환하는 것처럼 배열도 존재하지 않는 요소를 참조하면 undefined를 반환한다.

배열 요소의 추가와 갱신

객체에 프로퍼티를 동적으로 추가할 수 있는 것처럼 배열에도 요소를 동적으로 추가할 수 있다. 존재하지 않는 인덱스를 사용해 값을 할당하면 새로운 요소가 추가된다.

인덱스는 요소의 위치를 나타내므로 반드시 0 이상의 정수(또는 정수 형태의 문자열)을 사용해야 한다. 만약 정수 이외의 값을 인덱스로 사용하면 요소가 생성되는 것이 아니라 프로퍼티가 생성된다. 이때 추가된 프로퍼티는 length 프로퍼티 값에 영향을 주지 않는다.

const arr = [];

arr[0] = 1;
arr['1'] = 2;

arr['foo'] = 3;
arr.bar = 4;
arr[1.1] = 5;
arr[-1] = 6;

console.log(arr); // [1,2, foo: 3, bar: 4, '1.1': 5, '-1', 6]

// 프로퍼티는 length에 영향을 주지 않는다
console.log(arr.length); // 2

배열 요소의 삭제

배열은 사실 객체기 때문에 배열의 요소를 삭제하기 위해 delete 연산을 사용할 수 있다.

const arr = [1,2,3];

delete arr[1];
console.log(arr); // [1, empty, 3]

// length 프로퍼티에 영향을 주지 않는다. 즉, 희소 배열이 된다.
console.log(arr.length); // 3

따라서 희소 배열을 만드는 delete 연산자는 사용하지 않는 것이 좋다.

희소 배열을 만들지 않으면서 배열의 특정 요소를 완전히 삭제하려면 Array.prototype.splice 메서드를 사용한다.

const arr = [1,2,3];

// Array.prototype.splice (삭제를 시작할 인덱스, 삭제할 요소 수)
// arr[1]부터 1개의 요소 제거
arr.splice(1,1);
console.log(arr); // [1,3]

배열 메서드

배열에는 원본 배열을 직접 변경하는 메서드와 원본 배열을 직접 변경하지 않고 새로운 배열을 생성하여 반환하는 메서드가 있다.

const arr = [1];

arr.push(2);
console.log(arr); // [1,2]

// concat 메서드는 원본 배열을 직접 변경하지 않고 새로운 배열을 생성하여 반환한다.
const result = arr.concat(3);
console.log(arr); // [1,2]
console.log(result); // [1,2,3]

Array.prototype.push

push 메서드는 인수로 받은 모든 값을 원본 배열의 마지막 요소로 추가하고 변경된 length 프로퍼티 값을 반환한다. push 메서드는 원본을 직접 변경한다.

push 메서드는 성능면에서 좋지 않다. 마지막 요소로 추가할 요소가 하나뿐이라면 length 프로퍼티를 사용하여 배열의 마지막 요소로 추가하는게 더 빠르다.

push는 원본 배열을 직접 변경하는 부수 효과가 있으니, ES6 스프레드 문법을 사용하는 편이 좋다.

const arr = [1,2];

// ES6 스프레드 문법
const newArr = [...arr, 3];
console.log(newArr); // [1,2,3]

Array.prototype.pop

Array.prototype.unshift

unshift 메서드는 인수로 전달받은 모든 값을 원본 배열의 선두에 요소로 추가하고 변경된 length 프로퍼티 값을 반환한다. 원본 배열을 직접 변경한다.,

const arr = [1,2];
arr.unshift(3,4);

console.log(arr); // [3,4,1,2]
const arr = [1,2];
// ES6 
const newArr = [3, ...arr];
console.log(newArr); // [3,1,2]

Array.prototype.shift

shft는 원본 배열의 첫 번째 요소를 제거하고 제거한 요소 반환. 원본 배열이 빈 배열이면 undefined 반환.

Array.prototype.concat

concat 메서드는 인수로 전달된 값들을 원본 배열의 마지막 요소로 추가한 새로운 배열을 반환.

const arr1 = [1,2];
const arr2 = [3,4];

let result = arr1.concat(arr2);
console.log(result); // [1,2,3,4]

Array.prototype.splice

원본 배열 요소 제거

  • start: 원본 배열의 요소를 제거하기 시작할 인덱스. -1이면 마지막 요소, -n이면 마지막에서 n번째 요소
  • deleteCount: start부터 제거할 요소의 개수.
  • items: 제거한 위치에 삽입할 요소의 목록
const arr = [1,2,3,4];

const result = arr.splice(1, 2, 20, 30);

console.log(result); // [2,3]
console.log(arr); // [1, 20, 30, 4]

Array.prototype.slice

인수로 전달된 범위의 요소를 복사하여 배열로 반환. 원본 배열은 변경되지 않는다.

const arr = [1,2,3];

arr.slice(1); // [2,3]
const arr = [1,2,3];

// 인수를 모두 생략하면 원본 배열의 복사본을 생성하여 반화나.
const copy = arr.slice();
console.log(copy); // [1,2,3]
console.log(copy === arr); // false

이때 생성된 복사본은 얕은 복사를 통해 생성된다.

객체를 프로퍼티 값으로 갖는 객체의 경우 얕은 복사는 한 단계까지만 복사하는 것을 말하고, 깊은 복사는 객체에 중첩되어 있는 객체까지 모두 복사하는 것을 말한다.

const todos = [
  {id: 1, content: 'HTML', completed: false},
  {id: 2, content: 'CSS', completed: true},
  {id: 3, content: 'JavaScript', completed: false}
};

// 얕은 복사
const _todos = todos.slice();

// _todos와 todos는 참조값이 다른 별개의 객체다.
console.log(_todos === todos); // false

// 배열 요소의 참조값이 같다. 즉, 얕은 복사다.
console.log(_todos[0] === todos[0]); // true

Array.prototype.join

Array.prototype.reverse

Array.prototype.fill

Array.prototype.flat

[1, [2,3,4,5]].flat(); // [1,2,3,4,5]

중첩 배열을 평탄화할 깊이를 인수로 전달할 수 있다. 기본은 1이다.

배열 고차 함수

고차 함수는 함수를 인자로 전달받거나 함수를 반환하는 함수를 말한다. 고차 함수는 외부 상태의 변경이나 변경 데이터를 피하고 불변성을 지향하는 함수형 프로그래밍에 기반을 두고 있다.

함수형 프로그래밍은 순수 함수와 보조 함수의 조합을 통해 로직 내에 존재하는 조건문과 반복문을 제거하여 복잡성을 해결하고 변수의 사용을 억제하여 상태 변경을 피하려는 프로그래밍 패러다임이다. 함수형 프로그래밍은 결국 순수 함수를 통해 부수 효과를 최대한 억제하여 오류를 피하고 프로그램의 안정성을 높이려는 노력의 일환이다.

Array.prototype.sort

숫자 요소를 정렬할 때는 정렬 순서를 정의하는 비교 함수를 인수로 전달해야 한다.
비교 함수의 반환값이 0보다 작으면 비교 함수의 첫 번째 인수를 우선하여 정렬하고, 0이면 정렬하지 않으며, 0보다 크면 두 번째 인수를 우선하여 정렬한다.

const points = [40, 100, 1, 5, 2, 25, 10];
points.sort((a, b) => a - b);
console.log(points); // [1, 2, 5, 10, 25, 40, 100]

Array.prototype.forEach

forEach 메서드는 반복문을 추상화한 고차 함수로서 내부에서 반복문을 통해 자신을 호출한 배열을 순회하면서 수행해야 할 처리를 콜백 함수로 전달받아 반복 호출한다.

const numbers = [1,2,3];
const pows = [];

numbers.forEach(item => pows.push(item ** 2));
console.log(pows); // [1, 4, 9]
// forEach 메서드는 콜백 함수를 호출하면서 3개(요소값, 인덱스, this)의 인수를 전달한다.
[1,2,3].forEach((item, index, arr) => {
  console.log(`요소값: ${item}, 인덱스: ${index}, this: ${JSON.strinfy(arr)}`);
});

forEach 메서드는 원본배열을 변경하지 않는다. 하지만 콜백함수를 통해 원본 배열을 변경할 수는 있다. forEach 메서드의 반환값은 언제나 undefined 이다.

forEach 메서드의 콜백 함수는 일반 함수로 호출되므로 콜백 함수 내부의 this는 undefined를 가리킨다. this가 전역 객체가 아닌 undefined를 가리키는 이유는 클래스 내부의 모든 코드에는 암묵적으로 strict mode가 적용되기 때문이다.

forEach 메서드의 콜백 함수 내부의 this와 multiply 메서드 내부의 this를 일치시키려면 forEach 메서드의 두 번째 인수로 forEach 메서드 콜백 함수 내부에서 this로 사용할 객체를 전달한다.

class Numbers {
  numberArray = [];
  
  multiply(arr) {
    arr.forEach(function (item) {
      this.numberArray.push(item * item);
    }, this); // forEach 메서드의 콜백 함수 내부에서 this로 사용할 객체를 전달
  }
}

더 나은 방법은 ES6의 화살표 함수를 사용하는 것이다. 화살표 함수는 함수 자체의 this 바인딩을 갖지 않는다. 따라서 화살표 함수 내부에서 this를 참조하면 상위 스코프, 즉 multiply 메서드 내부의 this를 그대로 참조한다.

class Numbers {
  numberArray = [];
  multiply(arr) {
    // 화살표 함수에서 this를 참조하면 상위 스코프의 this를 그대로 참조
    arr.forEach(item => this.numberArray.push(item * item));
  }
}

Array.prototype.map

map 메서드는 자신을 호출한 배열의 모든 요소를 순회하면서 인수로 전달받은 콜백 함수를 반복 호출한다. 그리고 콜백 함수의 반환값들로 구성된 새로운 배열을 반환한다.

Array.prototype.filter

콜백 함수의 반환값이 true인 구성요소로만 구성된 새로운 배열을 반환한다. 원본 배열은 변경 x.

Array.prototype.reduce

콜백 함수의 반환값을 다음 순회 시에 콜백 함수의 첫 번째 인수로 전달하면서 콜백 함수를 호출하여 하나의 결과값을 만들어 반환한다.

reduce 메서드는 첫 번째 인수로 콜백 함수, 두 번째 인수로 초기값을 전달받는다. reduce 메서드의 콜백 함수에는 4개의 인수, 초기값 또는 콜백 함수의 이전 반환값, reduce 메서드를 호출한 배열의 요소값과 인덱스, reduce 메서드를 호출한 배열 자체, 즉 this가 전달된다.

1부터 4까지 누적

const sum = [1,2,3,4].reduce((accumulator, currentValue, index, array) => accumulator + currentValue, 0);

*최대값 구하기

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

const max = values.reduce((acc, cur) => (acc > cur ? acc : cur), 0);
console.log(max); // 5
const sum = [].reduce((acc, cur) => acc + cur);
// TypeError: Reduce of empty array with no initial value

이처럼 빈 배열로 reduce를 호출하면 에러가 발생한다. 이때 reduce 메서드에 초기값을 전달하면 에러가 발생하지 않는다. 객체의 특정 프로퍼티 값을 합산하는 경우에도 반드시 초기값을 전달해야 한다.

Array.prototype.some

콜백 함수의 반환값이 단 한번이라도 참이면 true, 모두 거짓이면 false를 반환한다.

[5, 10, 15].some(item => item > 10); // true

Array.prototype.every

콜백 함수의 반환값이 모두 참이면 true, 단 한번이라도 거짓이면 false를 반환한다.

Array.prototype.find

인수로 전달된 콜백 함수를 호추랗여 반환값이 true인 첫 번째 요소 반환. true인 요소가 없다면 undefined 반환.

Array.prototype.findIndex

콜백 함수 호출해서 반환값이 true인 첫 번째 요소 인덱스 반환. 없으면 -1

Array.prototype.flatMap

map 메서드와 flat 메서드를 순차적으로 실행하는 효과

0개의 댓글