배열 — 값을 순서대로 담는다
Article
지금까지 변수 하나에 값 하나를 담았다. 학생 서른 명의 점수를 다루려면 변수를 서른 개 만들어야 할까.
여러 값을 순서대로 하나에 담는 그릇이 배열이다.
만들고 꺼낸다
대괄호 [ ] 안에 값을 쉼표로 나열한다.
const fruits = ["사과", "바나나", "포도"];
console.log(fruits[0]);
console.log(fruits[2]);
console.log(fruits.length);
console.log(fruits[3]);
- '사과'
- '포도'
- 3
- undefined
5강의 문자열과 똑같다. 인덱스는 0부터 시작하고, 마지막 인덱스는 length - 1이다. 없는 자리를 꺼내면 에러가 아니라 undefined가 나온다.
- "사과"인덱스 0
- "바나나"인덱스 1
- "포도"인덱스 2
배열은 타입을 가리지 않는다. 섞어 담아도 된다.
const mixed = [1, "글자", true, null, [1, 2]];
console.log(mixed[4][1]);
console.log(typeof mixed);
console.log(Array.isArray(mixed));
- 2
- 'object'
- true
4강에서 본 대로 typeof는 'object'를 준다. 배열은 객체의 한 종류이기 때문이다. 배열인지 확인하려면 Array.isArray()를 쓴다.
문자열과 다른 점 — 배열은 바뀐다
5강에서 문자열은 고칠 수 없다고 했다. 배열은 다르다.
const word = "hello";
word[0] = "H";
console.log(word);
const arr = ["h", "e"];
arr[0] = "H";
console.log(arr);
- hello
- ['H', 'e']
배열은 자리에 값을 직접 넣을 수 있다. const로 선언했는데도 된다. 3강에서 예고한 그 이야기다.
이름과 배열의 연결만 잠겼다. 배열 내용은 열려 있다
- arr
- 0x1a2b
- ['H', 'e']
arr에 다른 배열을 통째로 넣는 것은 막히지만, 배열 안을 고치는 것은 막지 않는다.
값을 넣고 뺀다
끝과 앞에서 각각 넣고 뺀다. 네 개가 짝을 이룬다.
| 메서드 | 위치 | 하는 일 | 반환값 |
|---|---|---|---|
| push | 끝 | 넣는다 | 새 length |
| pop | 끝 | 뺀다 | 빼낸 값 |
| unshift | 앞 | 넣는다 | 새 length |
| shift | 앞 | 뺀다 | 빼낸 값 |
const stack = ["a", "b"];
stack.push("c");
console.log(stack);
const last = stack.pop();
console.log(last, stack);
stack.unshift("z");
console.log(stack);
- ['a', 'b', 'c']
- 'c' ['a', 'b']
- ['z', 'a', 'b']
push와 pop은 배열의 끝만 건드려서 빠르다. 13강에서 본 콜 스택이 정확히 이렇게 동작한다. 쌓고, 위에서부터 꺼낸다.
unshift와 shift는 앞에 넣고 빼므로 나머지 전부의 인덱스가 밀린다. 배열이 길면 그만큼 느려진다.
slice 와 splice — 헷갈리는 한 쌍
이름이 비슷한데 동작이 완전히 다르다. 하나는 원본을 지키고, 하나는 원본을 부순다.
slice
잘라서 복사한다
- 원본을 바꾸지 않는다
- 잘라낸 새 배열을 돌려준다
slice(시작, 끝)— 끝은 미포함- 인자 없이 부르면 전체 복사
splice
잘라내고 끼워 넣는다
- 원본을 직접 바꾼다
- 제거된 값들을 돌려준다
splice(시작, 개수, 넣을값…)- 삭제·삽입·교체를 모두 한다
const original = ["a", "b", "c", "d"];
const copied = original.slice(1, 3);
console.log(copied);
console.log(original);
const removed = original.splice(1, 2);
console.log(removed);
console.log(original);
- ['b', 'c']
- ['a', 'b', 'c', 'd']
- ['b', 'c']
- ['a', 'd']
돌려주는 값은 같지만, slice 뒤의 original은 멀쩡하고 splice 뒤의 original은 잘려나갔다.
splice는 넣기도 한다. 세 번째 인자부터가 끼워 넣을 값이다.
const items = ["a", "d"];
items.splice(1, 0, "b", "c");
console.log(items);
- ['a', 'b', 'c', 'd']
인덱스 1에서 0개를 지우고 "b", "c"를 넣었다.
원본을 바꾸는가, 아닌가
배열 메서드를 볼 때 가장 먼저 물어야 할 질문이다. 이 구분을 놓치면 4강에서 본 참조 타입 버그를 만난다.
| 원본을 바꾼다 | 원본을 지킨다 |
|---|---|
| push · pop | slice |
| unshift · shift | concat |
| splice | join |
| sort · reverse | indexOf · includes |
sort가 원본을 바꾼다는 걸 모르고 쓰다 사고가 난다.
const scores = [3, 1, 2];
const sorted = scores.sort();
console.log(sorted);
console.log(scores);
- [1, 2, 3]
- [1, 2, 3]
scores까지 정렬돼버렸다. 원본을 지키려면 복사한 뒤 정렬한다.
const scores = [3, 1, 2];
const sorted = scores.slice().sort();
console.log(sorted, scores);
- [1, 2, 3] [3, 1, 2]
찾는다
const fruits = ["사과", "바나나", "포도"];
console.log(fruits.indexOf("바나나"));
console.log(fruits.indexOf("수박"));
console.log(fruits.includes("포도"));
- 1
- -1
- true
indexOf는 못 찾으면 -1을 준다. 0이 falsy라는 걸 기억하면 왜 if (fruits.indexOf(x))가 위험한지 보인다. 첫 번째 요소를 찾아도 0이라 거짓 취급된다.
있는지만 알고 싶다면 includes를 쓴다. 의도가 분명하고 함정이 없다.
복사할 때 조심한다
4강의 그 이야기다.
const a = [1, 2, 3];
const b = a;
b.push(4);
console.log(a);
- [1, 2, 3, 4]
=로는 복사되지 않는다. 주소만 복사되어 둘이 같은 배열을 본다.
진짜 복사본을 만드는 방법은 여럿이다.
const a = [1, 2, 3];
const copy1 = a.slice();
const copy2 = [...a];
const copy3 = Array.from(a);
copy2.push(99);
console.log(a, copy2);
- [1, 2, 3] [1, 2, 3, 99]
[...a]가 스프레드 문법이다. 18강에서 제대로 다룬다. 지금은 배열을 펼쳐 새 배열에 담는다고 이해하면 된다.
정리하면
- 배열은 값을 순서대로 담고, 인덱스는 0부터 시작한다.
typeof는'object'를 준다. 배열 확인은Array.isArray()로 한다.const배열도 내용은 바뀐다.const는 이름과 배열의 연결만 지킨다.push/pop은 끝을,unshift/shift는 앞을 다룬다. 앞쪽 조작이 더 비싸다.slice는 원본을 지키고splice는 원본을 부순다.sort와reverse도 원본을 바꾼다.indexOf는 못 찾으면-1, 첫 요소면0이다. 존재 확인은includes를 쓴다.=로는 복사되지 않는다.slice()나[...arr]를 쓰되, 얕은 복사임을 기억한다.
다음 강의에서는 배열의 진짜 무기를 꺼낸다. map, filter, reduce다. 11강에서 배운 콜백이 여기서 빛을 발한다.