자바스크립트에서 배열은 배열 리터럴, Array 생성자 함수, Array.of 메서드, Array.from 메서드를 통해 생성할 수 있다. 배열을 생성하는 방법이 4가지나 있기 때문에 각각의 생성 방식과 특징을 제대로 이해해야 목적에 맞는 방법을 선택할 수 있을 것이다.
이 외에도 String.prototype.split 메서드처럼 배열을 반환하는 빌트인 객체의 메서드들도 있다. 각각의 배열 생성 방식의 특성을 살펴보고, 몇 가지 기본적이면서도 자주 쓸 수 있는 배열 생성 방법에 대해 알아보자.
1. 배열 리터럴
가장 단순하고 직관적인 배열 생성 방식이다.
거의 대부분의 브라우저에서 가장 성능이 좋은 배열 생성 방법이다.
const a = [];
const b = [1, 2, 3];
배열 리터럴 내부에서 Spread Syntax(스프레드 문법)을 사용하여 기존의 배열을 활용한 새로운 배열을 생성할 수 있다.
const a = [1, 2, 3];
const b = [4, 5, 6];
const c = [...a, ...b]; // [1, 2, 3, 4, 5, 6]
Spread Syntax는 이터러블을 대상으로 사용할 수 있으므로, 배열이 아닌 이터러블로 배열을 생성할 수 있다.
const a = 'Hello World';
const b = [...a]; // ['H', 'e', 'l', 'l', 'o', ' ', 'W', 'o', 'r', 'l', 'd']
2. Array 생성자 함수
length 값을 인수로 전달받아 해당 길이의 배열을 생성한다.
const a = new Array(10);
length의 값과 상관없이 빈 배열을 생성하며, 전달된 length 프로퍼티 값이 0이라면 빈 배열 리터럴([])과 동일하다.
const a = new Array(10);
const b = new Array(0);
const c = [];
const d = [1, 2, 3];
console.log(Object.getOwnPropertyDescriptors(a));
/*
{
length: {value: 10, ...}
}
*/
console.log(Object.getOwnPropertyDescriptors(b));
/*
{
length: {value: 0, ...}
}
*/
console.log(Object.getOwnPropertyDescriptors(c));
/*
{
length: {value: 0, ...}
}
*/
// 비어 있지 않은 배열은 index와 배열의 원소를 키와 값으로 갖는 프로퍼티가 존재한다.
console.log(Object.getOwnPropertyDescriptors(d));
/*
{
0: {value: 1, ...}
1: {value: 2, ...}
2: {value: 3, ...}
length: {value: 3, ...}
}
*/
인수를 아예 전달하지 않으면 length가 0인 빈 배열을 생성한다. 이는 new Array(0) 또는 빈 배열 리터럴([])과 동일하다.
const a = new Array();
console.log(Object.getOwnPropertyDescriptors(a));
/*
{
length: {value: 0, ...}
}
*/
인수를 2개 이상 전달하면 Array.of 메서드와 동일하게 동작한다. 즉, 인수들을 요소로 갖는 배열을 생성한다.
const a = new Array(1, 2, 3); // [1, 2, 3]과 동일하다.
console.log(Object.getOwnPropertyDescriptors(a));
/*
{
0: {value: 1, ...}
1: {value: 2, ...}
2: {value: 3, ...}
length: {value: 3, ...}
}
*/
new 연산자를 사용하지 않고 호출하여도 동일하게 동작한다.
const a = Array(2); // [], a.length = 2
const b = Array(1, 2, 3); // [1, 2, 3]
3. Array.of 메서드
전달된 인수를 요소로 갖는 배열을 생성한다.
Array 생성자 함수와의 차이점은 인수가 1개 일 때에도 해당 요소를 갖는 배열을 생성한다는 점이다.
const a = Array.of(1, 2, 'abc', {}); // [1, 2, 'abc', {}]
const b = Array.of(10); // [10];
const c = new Array(10); // [], c.length = 10
4. Array.from 메서드
유사 배열 객체(array-like objects) 또는 이터러블을 인수로 전달받아 배열을 생성한다.
const a = {length: 3, 0: 1, 1: 2, 2: 3}; // 유사 배열 객체
const b = Array.from(a); // [1, 2, 3]
const c = Array.from('abc'); // ['a', 'b', 'c']
Array.from의 두 번째 인수로 배열의 모든 요소에 대해 호출할 콜백 함수,
세 번째 인수로 콜백 함수 내부에서 this에 바인딩할 객체를 전달받을 수 있다.
const a = 'Hello World';
const b = Array.from(a, v => v.toLowerCase()); // ['h', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd']
활용
이제 위에서 살펴본 배열 생성 방식들을 활용하여 여러 가지 배열을 생성해보자.
① 1 ~ N 까지 자연수를 나열한 배열 생성 (규칙적인 수 나열)
// Array 생성자 함수
const a = [...new Array(10)].map((_, i) => i + 1); // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
// Array.from 메서드
const b= Array.from({length: 10}, (_, i) => i + 1); // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
위 코드에서 new Array(10)에 바로 map을 사용하지 않고 배열 내부에 Spread Operator를 사용한 이유는 new Array(10)은 length 프로퍼티 값만 10이고 실제로 내부에 든 요소가 없기 때문이다.
const a = new Array(5).map((_, i) => i + 1); // []
const b = [...new Array(5)]; // [undefined, undefined, undefined, undefined, undefined]
const c = [...new Array(5)].map((_, i) => i + 1); // [1, 2, 3, 4, 5]
// Array.from 메서드에 length 프로퍼티만 가진 유사 배열 객체를 전달하면
// 해당 길이 만큼의 undefined를 요소로 갖는 배열을 반환한다.
const d = Array.from({length: 5}); // [undefined, undefined, undefined, undefined, undefined]
const e = Array.from({length: 5}, (_, i) => i + 1); // [1, 2, 3, 4, 5]
둘 중에 어느 것을 쓰는게 더 좋은지는 잘 모르겠다.
1부터 1,000,000까지의 수를 나열하는 배열을 각각 생성하고 실행 시간을 측정하여 대략적으로 비교해 보았다.
console.time('Array 생성자 함수');
const a = [...new Array(1000000)].map((_, i) => i + 1);
console.timeEnd('Array 생성자 함수');
console.time('Array.from 메서드');
const b = Array.from({length: 1000000}, (_, i) => i + 1);
console.timeEnd('Array.from 메서드');
// 크롬 브라우저에서 측정
// Array 생성자 함수: 24.26708984375 ms
// Array.from 메서드: 65.093017578125 ms
위 코드의 콜백 함수를 변경함으로써 다양한 형태의 규칙적인 수를 나열하는 배열을 생성할 수 있다.
const a = [...Array(5)].map((_, i) => 2 * i + 1); // [1, 3, 5, 7, 9]
const b = [...Array(5)].map((_, i) => 2 * i + 2); // [2, 4, 6, 8, 10]
const c = [...Array(5)].map((_, i) => i * i); // [0, 1, 4, 9, 16]
const d = [...Array(5)].map((_, i) => 1 << i); // [1, 2, 4, 8, 16]
const e = [...Array(5)].map((_, i) => i % 2); // [0, 1, 0, 1, 0]
아래 코드는 MDN의 Array.from() 문서에 있는 Sequence generator 코드를 약간 수정하여
Python의 range와 유사하게 동작하도록 수정한 코드이다.
const range = (start, stop, step=1) => {
if (stop === undefined) {
return Array.from({length: start}, (_, i) => i * step);
}
if (step > 0) {
return Array.from({length: (stop - start + step - 1) / step}, (_, i) => start + (i * step));
}
if (step < 0) {
if (start <= stop) return [];
return Array.from({length: (start - stop - step - 1) / -step}, (_, i) => start + (i * step));
}
return [];
};
console.log(range(3)); // [0, 1, 2]
console.log(range(2, 4)); // [2, 3]
console.log(range(1, 10, 3)); // [1, 4, 7]
console.log(range(10, 1, -2)); // [10, 8, 6, 4, 2]
② 똑같은 요소로 채워진 배열 생성
Array.prototype.fill 메서드를 사용하면 전달받은 인수로 배열을 채울 수 있다.
const a = new Array(5).fill(0); // [0, 0, 0, 0, 0]
const b = [1, 2, 3, 4, 5].fill(0); // [0, 0, 0, 0, 0]
만약 객체를 전달받았다면, 그 참조만 복사해서 배열을 채우게 된다.
const a = new Array(5).fill([]); // [[], [], [], [], []]
a[3].push(1); // [[1], [1], [1], [1], [1]]
따라서 빈 배열로 배열을 채우려고 한다면 Array.prototype.fill 메서드 대신 다른 방법을 사용해야 한다.
const a = [...new Array(5)].map(() => []); // [[], [], [], [], []]
a[3].push(1); // [[], [], [], [1], []]
const b = Array.from({length: 5}, () => []); // [[], [], [], [], []]
b[3].push(1); // [[], [], [], [1], []]
String.prototype.repeat 메서드로 만든 반복되는 문자열로 배열을 채울 수도 있다.
하지만 속도가 다소 느리기 때문에 Array.prototype.fill 메서드를 사용하는 것을 추천한다.
const a = 'a'.repeat(5).split(''); // ['a', 'a', 'a', 'a', 'a']
const b = Array.from('a'.repeat(5)); // ['a', 'a', 'a', 'a', 'a']
const c = new Array(...'a'.repeat(5)); // ['a', 'a', 'a', 'a', 'a']
const d = Array.of(...'a'.repeat(5)); // ['a', 'a', 'a', 'a', 'a']
// 대략적인 속도 측정 결과
// a는 new Array(5).fill('a') 와 거의 비슷하고
// b는 실행할 때마다 속도의 우위가 달라지지만 대체로 a보다 느리다.
// c, d는 훨씬 더 느리다.
두서 없이 글을 쓰는 바람에 글의 내용이 다소 난해해 졌는데, 원래 쓰려던 내용을 요약하자면 이렇다.
1. 배열 리터럴은 성능이 가장 좋은 배열 생성 방법이다.
2. 규칙적인 수들이 나열된 배열을 바로 생성할 때에는 Array 생성자 함수를 사용하자.
3. 유사 배열 객체 또는 이터러블로 배열을 생성하고자 할 때에는 Array.from 메서드를 사용하자.
4. 이터러블로 배열을 생성할 때 또는 배열을 합칠 때에는 배열 리터럴과 스프레드 연산자를 사용하자.
자바스크립트 배열은 다양하고 강력한 메서드들을 제공하고 이를 응용하여 다양한 기능을 구현할 수 있다. 이번 글의 주제는 자바스크립트의 배열 생성 방법이고, 배열 리터럴 외에 다른 생성 방법에 대해 알아보고 어떻게 사용할 수 있을지 고민해보기 위해 썼기 때문에 다음에 더 다루도록 하겠다. 위에서 다룬 두 가지 활용 방법만 알아도 알고리즘 문제를 풀 때 유용하게 쓸 수 있을 것이다.
참고자료
『모던 자바스크립트 Deep Dive』 - 이웅모(2021)
'IT > JavaScript' 카테고리의 다른 글
[JS] 자바스크립트로 엑셀 파일(xlsx) 만들어서 다운받기 (0) | 2022.08.05 |
---|---|
[JS] Invalid regular expression: invalid group specifier name (0) | 2022.06.21 |
[JS] Selection API로 선택된 텍스트 정보 가져오기 (0) | 2022.06.01 |
[JS] 함수의 매개변수와 RangeError (0) | 2021.07.23 |
Node.js로 백준(BOJ) 문제 풀 때 유의할 점들 (12) | 2021.04.10 |