자바스크립트 완벽 가이드 7장 정리 ( Array )
포스트
취소

자바스크립트 완벽 가이드 7장 정리 ( Array )

해당 포스트는 자바스크립트 완벽 가이드라는 교재로 스터디를 하면서 7장을 정리한 포스트입니다.
주관적으로 해석한 내용이 들어가 있어서 잘못된 내용이 포함될 수 있습니다.
또한 교재의 모든 내용을 정리하지 않고 주관적인 판단에 의해 필요한 내용만 작성했습니다.

✍️ 배열 생성 방법

0️⃣ 배열 리터럴

1
2
3
const arr = [1,,2,]; // 성긴 배열

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

1️⃣ 이터러블인 객체와 spread operator

이터러블이 궁금하다면 JavaScript의 배열은 어떻게 동작할까?를 참고해주세요!

이터러블이라면 spread operator를 이용해서 쉽게 배열로 변환할 수 있습니다.

1
2
3
const arr = [1,,3,]; // 성긴 배열

console.log(...arr); // 1 undefined 2

문자열로 이터러블이기 때문에 아래처럼((1)) 사용이 가능합니다.

1
2
3
4
const fruit = "apple";

// (1)
const arr = [...fruit]; // ["a", "p", "p", "l", "e"]

Map, Set도 이터러블이라서 전개 연산자와 함께 사용하면 배열로 생성할 수 있고, Set은 중복을 허용하지 않기 때문에 배열에서 중복된 데이터를 지울 때 사용합니다.
또한 불변성을 지킬 때 사용하기 가장 간단한 방법이라고 생각해서 개인적으로 React에서 정말 많이 사용하는 방법입니다.

2️⃣ 생성자 함수

생성자 함수를 이용하면 길이를 자유롭게 배열을 만들 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// ========== 생성자 함수 ==========
const arr = Array(1, 2, 3);
console.log(arr); // [1, 2, 3]

// ========== 생성자 함수 ( 단일 인자 ) ==========
const arr2 = Array(3);
console.log(arr2); // [empty X 3]
console.log(arr2.fill(null)); // [null, null, null]

// ========== 단일 인자 배열 생성 ==========
const arr3 = Array.of(3);
console.log(arr3); // [3]

// ========== 유사 배열 객체를 배열로 변환 ==========
const func = function () {
  const arr = Array.from(arguments);
  console.log(arr);
}
func(1,2,3,4); // [1,2,3,4]

생성자 함수와 배열 메서드를 이용해서 규칙있는 값이 들어간 배열 만드는 방법입니다.
( 제가 자주 사용하는 방법입니다. )
아래처럼 배열을 만들면 for문을 쓰지 않고도 원하는 만큼 반복할 수 있습니다.

1
2
3
4
5
const candidate = Array(20).fill(null).map((v, i) => v + 1);
// [1, 2, 3, ... , 19, 20]

// (2)
candidate.forEach(v => /* ...특정 작업 */);

(2)for문 보다는 비효율적이지만 저는 이게 더 가독성이 좋고 편하다고 생각해서 이 방법만 사용하고 있습니다.
( 어느 정도의 편의를 위해서는 조금의 비효율적인 행위는 이해해도 된다고 생각합니다. )

3️⃣ Array.form()

배열 생성자 함수의 정적 메서드인 Array.form()을 사용하면 유사 배열 객체를 배열로 변환할 수 있습니다.

  • 유사 배열 객체의 대표적인 예시
    1. functionarguments
    2. HTMLCollection
1
2
3
4
5
6
7
function func() {
  // (3)
  console.log(Array.isArray(Array.form(arguments))); // true

  // (4)
  console.log(Array.isArray(...arguments));
}

(3)처럼 사용이 가능하지만, 저는 (4)가 더 편해서 (4)를 더 애용합니다.

👀 배열 접근

대괄호([])를 이용해서 접근합니다.
그리고 일반적으로는 숫자를 인덱스로 사용하고 실행할 때 문자로 변환되어 실행됩니다.
즉, 숫자로 된 문자열로도 접근할 수 있다는 의미죠.( 하지만 굳이 그렇게 사용할 필요는 없겠죠. )

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

console.log(arr[2] === arr["2"]); // true

그리고 JavaScript의 배열은 경계 초과 에러가 발생하지 않습니다.

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

console.log(arr[10]); // undefined

🚄 배열 메서드

성긴 배열의 경우에는 존재하지 않는 요소(<empty>)에 대해서는 반복을 수행하지 않습니다.
즉, 배열 메서드를 사용하는 경우 존재하지 않는 요소(<empty>)에 대해서는 취급하지 않고 무시합니다.

1
2
3
4
5
6
7
8
9
10
11
12
const arr1 = [1, , , 3];
const arr2 = [1, undefined, undefined, 3];

console.group("성긴배열 시작");
arr1.forEach((v) => console.log(v)); // 1 3 
console.groupEnd("성긴배열 끝");

console.group("udnefined 배열 시작");
arr2.forEach((v) => console.log(v)); // 1 undefined undefined 3
console.groupEnd("udnefined 배열 끝");

console.log("<empty>의 타입 >> ", typeof arr2[1]); // undefined

0️⃣ 여러가지 배열 메서드

자세한 내용은 직접 구현한 배열 메서드들을 참고해주세요.

자주 사용하는 배열 메서드들에 대한 내용은 제가 설명하는 것보다는 MDN에서 찾아보는 것이 가장 좋습니다.

그래도 너무 설명을 안 하면 아쉬우니 제가 직접 구현해본 배열 메서드을 한 번 읽어보고 직접 구현해보면 좋겠다고 생각해서 링크를 첨부했습니다.

🫢 배열 비슷한 객체

JavaScript의 배열은 특별한 객체 즉, 객체지만 특별한 기능들이 있습니다.
배열 메서드들이 그 특별한 기능을 사용할 수 있게 도와줍니다.

그런데 신기한점은 유사 배열 객체에서도 배열처럼 배열 메서드를 사용할 수 있습니다.
( key가 숫자인 문자열이면서 length가 숫자라면 배열 메서드를 사용할 수 있습니다. )
( 배열 비슷한 객체 즉, 유사 배열 객체는 length에 의해 변경되는 로직이 추가되어야 합니다. )

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const arrLike = {};

for (let i = 0; i < 10; i++) {
  arrLike[i] = "a" + i;
}

arrLike.length = 10;

// (1)
console.log(Array.prototype.myJoin.call(arrLike, "+"));

// (2)
Array.prototype.push.call(arrLike, "100");
console.log(arrLike.length); // 11

(1)처럼 직접 만든 배열 메서드로도 동작합니다.( 내부적으로 for문이 돌아가기 때문이죠. )
단, 배열의 prototype체인에 연결되어 있지 않기 때문에 this 바인딩 함수들을 이용해서 사용해야 합니다.

그리고 (2)처럼 원본 배열을 바꾸는 메서드를 사용하면 length 속성도 바뀌는 것으로 봐서는 원본 배열을 바꾸는 메서드 내부적으로 length를 변경시켜주는 로직이 들어가 있는 것으로 보입니다. ( 확실하지 않음 )

🧐 배열의 특이점

0️⃣ 배열 접근 속도 비교

JavaScript의 배열은 특정 방식으로 최적화되어서 숫자로 접근하는 요소들은 일반 객체의 프로퍼티보다 빠르게 접근할 수 있습니다.

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
let count = 10_000_000;

const arr = Array(count)
  .fill(0)
  .map((v, i) => i);

// =========================================

let sum = 0;
let start = Date.now();

for (let i = 0; i < arr.length; i++) {
  sum += arr[i];
}

console.log("for >> ", Date.now() - start); // 20

// =========================================

sum = 0;
start = Date.now();

for (let i = 0, j = arr.length; i < j; i++) {
  sum += arr[i];
}

console.log("최적화 for >> ", Date.now() - start); // 11

// =========================================

sum = 0;
start = Date.now();

for (const key in arr) {
  sum += arr[key];
}

console.log("for in >> ", Date.now() - start); // 2700

// =========================================

sum = 0;
start = Date.now();

for (const key of arr) {
  sum += key;
}

console.log("for of >> ", Date.now() - start); // 140

결과를 요약하면 다음과 같습니다.

  1. for -> 20
  2. 최적화 for -> 10
  3. for in -> 2800
  4. for of -> 140 ( 단, 인덱스를 접근할 수 없음 )

사실 for of가 이터러블에 최적화된 반복문이라 제일 빠를 줄 알았는데 생각보다 느리네요.
그리고 for문이 압도적으로 빨라서 실행 속도가 중요하다면 for을 쓰는 것이 좋아보입니다.
그리고 최적화 forfor의 속도 차이가 2배가 나긴 하지만 교재에서 말하기를 최신 JavaScript엔진에서는 성능 차이는 거의 없다고 했기 때문에 큰 차이는 없는 것 같습니다.

1️⃣ 인덱스

배열도 객체이기 때문에 인덱스도 모두 key로 저장됩니다.

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

for(let k in arr) {
  console.log(k); // "0" "1" "2"
}

// (1) length가 "for ~ in"에서 이유는 "enumerable: false"이기 때문입니다.
Object.getOwnPropertyDescriptor(arr, "length"); // {value: 3, writable: true, enumerable: false, configurable: false}

2️⃣ length property

배열의 length는 기본적으로 내부 요소의 변화에 맞게 값이 변화됩니다.
근데 독특한 점이 쓰기가 가능하다는 점입니다.
위 예시의 (1)에서 보면 알 수 있듯이 writable: true입니다.

1
2
3
4
5
6
7
8
9
const arr = [1,2,3];

// (1)
arr.length = 0;
console.log(arr); // []

// (2)
arr.length = 3;
console.log(arr); // [empty X 3]

(1)(2)처럼 length를 변화시키면 그에 맞게 요소가 제거되거나 추가(<empty>)됩니다.

  • 내가 생각하는 length 동작 방식
    1. 배열이 생성되면 그에 맞게 length 값 초기화
    2. length를 변경하면 그에 맞게 요소 제거/추가
    3. 원본 배열을 수정하는 메서드를 사용하면 내부적으로 length 조절 ( 유사 배열 객체에 원본 배열을 수정하는 메서드를 사용하면 length가 변함 ( 근거: 예시(2)참고 ) )

3️⃣ 성긴 배열

배열 내부의 요소들이 이어질 필요 없고 그 사이에 갭이 있을 수 있는 배열입니다.
성긴 배열의 요소 검색 시간은 객체의 property 검색 시간과 비슷합니다.
( 일반 배열은 어떤 최적화를 통해 검색 속도가 매우 빠름 )
( 성긴 배열도 undefined가 들어가 있는 것으로 적용돼서 일반 배열처럼 빠르게 동작함 )

1
2
3
4
5
6
7
8
9
10
11
12
13
const arr = [1,,3];
console.log(arr); // [1, empty, 3]

const arr2 = [1];
arr2[5] = 5;
console.log(arr2); // [1, empty X 4, 5]

const arr3 = [1,2,3];
delete arr3[1]; // "delete"는 배열의 길이에 변화를 주지 않음
console.log(arr3); // [1, empty, 3]

const arr4 = Array(10);
console.log(arr4); // [empty X 10]

empty라는 존재는 비어있음을 표현하는 값인 것 같습니다.
따라서 in, for ~ in, 배열 메서드를 통한 연산에 적용되지 않습니다.

1
2
3
4
5
6
7
8
9
10
11
12
const arr = ["a",,"c"];
console.log(0 in arr); // true
console.log(1 in arr); // false
console.log(2 in arr); // true

for(let k in arr) {
  console.log(k);
}
/**
 * "0"
 * "2"
 */

4️⃣ delete

delete를 이용해서 배열의 요소를 제거하는 경우 특이한 점이 두 가지 있습니다.

  • 요소만 제거할 뿐 길이가 줄어들지 않습니다. ( 즉, length가 변하지 않음 )
1
2
3
4
5
6
const arr = [1, 2, 3];

delete arr[2];

console.log(arr.length); // 3
console.log(arr); // [1, 2, <empty>]
  • 빈 공간을 채우기 위해서 요소가 이동하지 않습니다.

JavaScript의 배열은 다른 언어의 일반적인 배열과 다르게 Linked List와 같아서 연속적인 메모리 공간에 배치되지 않습니다.
따라서 중간에 어떤 요소가 제거되다고 하더라도 그 공백을 채우기 위해 다른 요소들이 움직일 필요없이 가리키는 주솟값만 변경시켜주면 된다는 의미인 것 같습니다.

FIXME: Linked List 정리한 후 링크 추가하기

❓ 의문

  1. 배열의 length의 동작 방식
  2. undefined<empty>를 구분하는 방법 ( 배열 메서드 직접 구현하는 경우 사용 )

💡 Tip

0️⃣ 배열 destructuring 활용

배열 destructuring을 이용하면 새로운 변수 없이 간단하게 값을 교환할 수 있습니다.

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

[arr[0], arr[1]] = [arr[1], arr[0]];

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

React.jsuseState()destructuring을 이용해서 사용합니다.

1
const [name, setName] = useState("alice");

📮 레퍼런스

  1. « 자바스크립트 완벽 가이드 7장 » ( 데이비드 플래너건 지음, 한성용 옮김, 인사이트, 2022 )
  2. 1-blue - JavaScript의 배열은 어떻게 동작할까?
  3. 1-blue - 직접 구현한 배열 메서드들
  4. MDN - Array
  5. 1-blue - this 바인딩 함수들
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.

이벤트 루프와 태스크

async & (callback | promise | async/await)