📢 22/05/26 복습
javascript.info, https://ko.javascript.info/array-methods
참고 사이트에 내용을 개인적으로 복습하기 편하도록 재구성한 글입니다.
자세한 설명은 참고 사이트를 살펴보시기 바랍니다.
배열에서 요소를 하나만 지우고 싶다면 어떻게 해야 할까요? 배열 역시 객체형에 속하므로 프로퍼티를 지울 때 쓰는 연산자 delete
를 사용해 볼 수 있을 겁니다.
let arr = ["I", "go", "home"];
delete arr[1]; // "go"를 삭제합니다.
alert( arr[1] ); // undefined
// delete를 써서 요소를 지우고 난 후 배열 --> arr = ["I", , "home"];
alert( arr.length ); // 3
원하는 대로 요소를 지웠지만 배열의 요소는 여전히 세 개이네요. arr.length == 3
을 통해 이를 확인할 수 있습니다.
이는 자연스러운 결과입니다. delete obj.key
는 key
를 이용해 해당 키에 상응하는 값을 지우기 때문이죠. delete
메서드는 제 역할을 다 한 것입니다. 그런데 우리는 삭제된 요소가 만든 빈 공간을 나머지 요소들이 자동으로 채울 것이라 기대하며 이 메서드를 썼습니다. 배열의 길이가 더 짧아지길 기대하며 말이죠.
이런 기대를 충족하려면 특별한 메서드를 사용해야 합니다.
arr.splice(start)
는 만능 스위스 맥가이버 칼 같은 메서드입니다. 요소를 자유자재로 다룰 수 있게 해주죠. 이 메서드를 사용하면 요소 추가, 삭제, 교체가 모두 가능합니다.
arr.splice(index[, deleteCount, elem1, ..., elemN])
첫 번째 매개변수는 조작을 가할 첫 번째 요소를 가리키는 인덱스(index)입니다. 두 번째 매개변수는 deleteCount
로, 제거하고자 하는 요소의 개수를 나타냅니다. elem1, ..., elemN
은 배열에 추가할 요소를 나타냅니다.
let arr = ["I", "study", "JavaScript"];
arr.splice(1, 1); // 인덱스 1부터 요소 한 개를 제거
alert( arr ); // ["I", "JavaScript"]
인덱스 1
이 가리키는 요소부터 시작해 요소 한 개를 지웠습니다.
요소 세 개를 지우고, 그 자리를 다른 요소 두 개로 교체해 보도록 하겠습니다.
let arr = ["I", "study", "JavaScript", "right", "now"];
// 처음(0) 세 개(3)의 요소를 지우고, 이 자리를 다른 요소로 대체합니다.
arr.splice(0, 3, "Let's", "dance");
alert( arr ) // now ["Let"s", "dance", "right", "now"]
splice
는 삭제된 요소로 구성된 배열을 반환합니다.
let arr = ["I", "study", "JavaScript", "right", "now"];
// 처음 두 개의 요소를 삭제함
let removed = arr.splice(0, 2);
alert( removed ); // "I", "study" <-- 삭제된 요소로 구성된 배열
splice
메서드의 deleteCount
를 0
으로 설정하면 요소를 제거하지 않으면서 새로운 요소를 추가할 수 있습니다.
let arr = ["I", "study", "JavaScript"];
// 인덱스 2부터
// 0개의 요소를 삭제합니다.
// 그 후, "complex"와 "language"를 추가합니다.
arr.splice(2, 0, "complex", "language");
alert( arr ); // "I", "study", "complex", "language", "JavaScript"
🔥 음수 인덱스도 사용할 수 있습니다.
splice
메서드 뿐만 아니라 배열 관련 메서드엔 음수 인덱스를 사용할 수 있습니다. 이때 마이너스 부호 앞의 숫자는 배열 끝에서부터 센 요소 위치를 나타냅니다.
let arr = [1, 2, 5];
// 인덱스 -1부터 (배열 끝에서부터 첫 번째 요소)
// 0개의 요소를 삭제하고
// 3과 4를 추가합니다.
arr.splice(-1, 0, 3, 4);
alert( arr ); // 1,2,3,4,5
arr.slice
는 arr.splice
와 유사해 보이지만 훨씬 간단합니다.
arr.slice([start], [end])
이 메서드는 "start"
인덱스부터 ("end"
를 제외한) "end"
인덱스까지의 요소를 복사한 새로운 배열을 반환합니다. start
와 end
는 둘 다 음수일 수 있는데 이땐, 배열 끝에서부터의 요소 개수를 의미합니다.
arr.slice
는 문자열 메서드인 str.slice
와 유사하게 동작하는데 arr.slice
는 서브 문자열(substring) 대신 서브 배열(subarray)을 반환한다는 점이 다릅니다.
let arr = ["t", "e", "s", "t"];
alert( arr.slice(1, 3) ); // e,s (인덱스가 1인 요소부터 인덱스가 3인 요소까지를 복사(인덱스가 3인 요소는 제외))
alert( arr.slice(-2) ); // s,t (인덱스가 -2인 요소부터 제일 끝 요소까지를 복사)
arr.slice()
는 인자를 하나도 넘기지 않고 호출하여 arr
의 복사본을 만들 수 있습니다. 💥 이런 방식은 기존의 배열을 건드리지 않으면서 배열을 조작해 새로운 배열을 만들고자 할 때 자주 사용됩니다.
arr.concat
은 기존 배열의 요소를 사용해 새로운 배열을 만들거나 기존 배열에 요소를 추가하고자 할 때 사용할 수 있습니다.
arr.concat(arg1, arg2...)
인자엔 배열이나 값이 올 수 있는데, 인자 개수엔 제한이 없습니다.
메서드를 호출하면 arr
에 속한 모든 요소와 arg1
, arg2
등에 속한 모든 요소를 한데 모은 새로운 배열이 반환됩니다.
인자 argN
가 배열일 경우 배열의 모든 요소가 복사됩니다. 그렇지 않은경우(단순 값인 경우)는 인자가 그대로 복사됩니다.
let arr = [1, 2];
// arr의 요소 모두와 [3,4]의 요소 모두를 한데 모은 새로운 배열이 만들어집니다.
alert( arr.concat([3, 4]) ); // 1,2,3,4
// arr의 요소 모두와 [3,4]의 요소 모두, [5,6]의 요소 모두를 모은 새로운 배열이 만들어집니다.
alert( arr.concat([3, 4], [5, 6]) ); // 1,2,3,4,5,6
// arr의 요소 모두와 [3,4]의 요소 모두, 5와 6을 한데 모은 새로운 배열이 만들어집니다.
alert( arr.concat([3, 4], 5, 6) ); // 1,2,3,4,5,6
concat
메서드는 제공받은 배열의 요소를 복사해 활용합니다. 객체가 인자로 넘어오면 (배열처럼 보이는 유사 배열 객체이더라도) 객체는 분해되지 않고 통으로 복사되어 더해집니다.
let arr = [1, 2];
let arrayLike = {
0: "something",
length: 1
};
alert( arr.concat(arrayLike) ); // 1,2,[object Object]
그런데 인자로 받은 유사 배열 객체에 특수한 프로퍼티 Symbol.isConcatSpreadable
이 있으면 concat
은 이 객체를 배열처럼 취급합니다. 따라서 객체 전체가 아닌 객체 프로퍼티의 값이 더해집니다.
let arr = [1, 2];
let arrayLike = {
0: "something",
1: "else",
[Symbol.isConcatSpreadable]: true,
length: 2
};
alert( arr.concat(arrayLike) ); // 1,2,something,else
arr.forEach
는 주어진 함수를 배열 요소 각각에 대해 실행할 수 있게 해줍니다.
arr.forEach(function(item, index, array) {
// 요소에 무언가를 할 수 있습니다.
});
아래는 요소 모두를 얼럿창을 통해 출력해주는 코드입니다.
// for each element call alert
["Bilbo", "Gandalf", "Nazgul"].forEach((item) => alert(item));
아래는 인덱스 정보까지 더해서 출력해주는 좀 더 정교한 코드입니다.
["Bilbo", "Gandalf", "Nazgul"].forEach((item, index, array) => {
alert(`${item} is at index ${index} in ${array}`);
});
참고로, 💥 인자로 넘겨준 함수의 반환값은 무시됩니다.
arr.indexOf
와 arr.lastIndexOf
, arr.includes
는 같은 이름을 가진 문자열 메서드와 문법이 동일합니다. 물론 하는 일도 같습니다. 연산 대상이 문자열이 아닌 배열의 요소라는 점만 다릅니다.
📌 arr.indexOf(item, from)
는 인덱스 from
부터 시작해 item
(요소)을 찾습니다. 요소를 발견하면 해당 요소의 인덱스를 반환하고, 발견하지 못했으면 -1
을 반환합니다.
📌 arr.lastIndexOf(item, from)
는 위 메서드와 동일한 기능을 하는데, 검색을 끝에서부터 시작한다는 점만 다릅니다.
📌 arr.includes(item, from)
는 인덱스 from
부터 시작해 item
이 있는지를 검색하는데, 해당하는 요소를 발견하면 true
를 반환합니다.
let arr = [1, 0, false];
alert( arr.indexOf(0) ); // 1
alert( arr.indexOf(false) ); // 2
alert( arr.indexOf(null) ); // -1
alert( arr.includes(1) ); // true
위 메서드들은 요소를 찾을 때 완전 항등 연산자 ===
을 사용한다는 점에 유의하시기 바랍니다. 보시는 바와 같이 false
를 검색하면 정확히 false
만을 검색하지, 0
을 검색하진 않습니다.
요소의 위치를 정확히 알고 싶은게 아니고 요소가 배열 내 존재하는지 여부만 확인하고 싶다면 arr.includes
를 사용하는 게 좋습니다.
includes
는 NaN
도 제대로 처리한다는 점에서 indexOf
, lastIndexOf
와 약간의 차이가 있습니다.
const arr = [NaN];
alert( arr.indexOf(NaN) ); // -1 (완전 항등 비교 === 는 NaN엔 동작하지 않으므로 0이 출력되지 않습니다.)
alert( arr.includes(NaN) );// true (NaN의 여부를 확인하였습니다.)
객체로 이루어진 배열이 있다고 가정해 봅시다. 특정 조건에 부합하는 객체를 배열 내에서 어떻게 찾을 수 있을까요? 이럴 때 arr.find(fn)
을 사용할 수 있습니다.
let result = arr.find(function(item, index, array) {
// true가 반환되면 반복이 멈추고 해당 요소를 반환합니다.
// 조건에 해당하는 요소가 없으면 undefined를 반환합니다.
});
요소 전체를 대상으로 함수가 순차적으로 호출됩니다.
함수가 참을 반환하면 탐색은 중단되고 해당 요소가 반환됩니다. 원하는 요소를 찾지 못했으면 undefined
가 반환됩니다.
id
와 name
프로퍼티를 가진 사용자 객체로 구성된 배열을 예로 들어보겠습니다. 배열 내에서 id == 1
조건을 충족하는 사용자 객체를 찾아봅시다.
let users = [
{id: 1, name: "John"},
{id: 2, name: "Pete"},
{id: 3, name: "Mary"}
];
let user = users.find(item => item.id == 1);
alert(user.name); // John
실무에서 객체로 구성된 배열을 다뤄야 할 일이 잦기 때문에 find
메서드 활용법을 알아두면 좋습니다.
arr.findIndex
는 find
와 동일한 일을 하나, 조건에 맞는 요소를 반환하는 대신 해당 요소의 인덱스를 반환한다는 점이 다릅니다. 조건에 맞는 요소가 없으면 -1
이 반환됩니다.
find
메서드는 함수의 반환 값을 true
로 만드는 단 하나의 요소를 찾습니다.
조건을 충족하는 요소가 여러 개라면 arr.filter(fn)
를 사용하면 됩니다.
filter
는 find
와 문법이 유사하지만, 조건에 맞는 요소 전체를 담은 배열을 반환한다는 점에서 차이가 있습니다.
let results = arr.filter(function(item, index, array) {
// 조건을 충족하는 요소는 results에 순차적으로 더해집니다.
// 조건을 충족하는 요소가 하나도 없으면 빈 배열이 반환됩니다.
});
let users = [
{id: 1, name: "John"},
{id: 2, name: "Pete"},
{id: 3, name: "Mary"}
];
// 앞쪽 사용자 두 명을 반환합니다.
let someUsers = users.filter(item => item.id < 3);
alert(someUsers.length); // 2
arr.map
은 유용성과 사용 빈도가 아주 높은 메서드 중 하나입니다.
map
은 배열 요소 전체를 대상으로 함수를 호출하고, 함수 호출 결과를 배열로 반환해줍니다.
let result = arr.map(function(item, index, array) {
// 요소 대신 새로운 값을 반환합니다.
});
아래 예시에선 각 요소(문자열)의 길이를 출력해줍니다.
let lengths = ["Bilbo", "Gandalf", "Nazgul"].map(item => item.length);
alert(lengths); // 5,7,6
arr.sort()
는 배열의 요소를 정렬해줍니다. 💥 배열 자체가 변경됩니다.
메서드를 호출하면 재정렬 된 배열이 반환되는데, 이미 arr
자체가 수정되었기 때문에 반환 값은 잘 사용되지 않는 편입니다.
let arr = [ 1, 2, 15 ];
// arr 내부가 재 정렬됩니다.
arr.sort();
alert( arr ); // 1, 15, 2
재정렬 후 배열 요소가 1
, 15
, 2
가 되었습니다. 기대하던 결과(1
, 2
, 15
)와는 다르네요. 왜 이런 결과가 나왔을까요?
모든 요소는 문자열로 취급되어 재 정렬되기 때문입니다. 문자열 비교는 사전편집 순으로 진행되기 때문에 2
는 15
보다 큰 값으로 취급됩니다("2" > "15"
).
기본 정렬 기준 대신 새로운 정렬 기준을 만들려면 arr.sort()
에 새로운 함수를 넘겨줘야 합니다.
인자로 넘겨주는 함수는 반드시 값 두 개를 비교해야 하고 반환 값도 있어야 합니다.
// 오름차순
function compareUp(a, b) {
if (a > b) return 1; // 첫 번째 값이 두 번째 값보다 큰 경우
if (a == b) return 0; // 두 값이 같은 경우
if (a < b) return -1; // 첫 번째 값이 두 번째 값보다 작은 경우
}
// 내림차순
function compareUp(a, b) {
if (a > b) return -1; // 첫 번째 값이 두 번째 값보다 큰 경우
if (a == b) return 0; // 두 값이 같은 경우
if (a < b) return 1; // 첫 번째 값이 두 번째 값보다 작은 경우
}
이제 배열 요소를 숫자 오름차순 기준으로 정렬해봅시다.
function compareNumeric(a, b) {
if (a > b) return 1;
if (a == b) return 0;
if (a < b) return -1;
}
let arr = [ 1, 2, 15 ];
arr.sort(compareNumeric);
alert(arr); // 1, 2, 15
여기서 잠시 멈춰 위 예시에서 어떤 일이 일어났는지 생각해 봅시다. 사실 arr
엔 숫자, 문자열, 객체 등이 들어갈 수 있습니다. 알 수 없는 무언가로 구성된 집합이 되는 거죠. 이제 이 비동질적인 집합을 정렬해야 한다고 가정해봅시다. 무언가를 정렬하려면 기준이 필요하겠죠? 이때 정렬 기준을 정의해주는 함수(ordering function, 정렬 함수) 가 필요합니다. sort
에 정렬 함수를 인자로 넘겨주지 않으면 이 메서드는 사전편집 순으로 요소를 정렬합니다.
arr.sort(fn)
는 포괄적인 정렬 알고리즘을 이용해 구현되어있습니다. 대개 최적화된 퀵 소트(quicksort)를 사용하는데, arr.sort(fn)
는 주어진 함수를 사용해 정렬 기준을 만들고 이 기준에 따라 요소들을 재배열하므로 개발자는 내부 정렬 동작 원리를 알 필요가 없습니다. 우리가 해야 할 일은 정렬 함수 fn
을 만들고 이를 인자로 넘겨주는 것뿐입니다.
정렬 과정에서 어떤 요소끼리 비교가 일어났는지 확인하고 싶다면 아래 코드를 활용하시면 됩니다.
[1, -2, 15, 2, 0, 8].sort(function(a, b) {
alert( a + " <> " + b );
return a - b;
});
정렬 중에 한 요소가 특정 요소와 여러 번 비교되는 일이 생기곤 하는데 비교 횟수를 최소화 하려다 보면 이런 일이 발생할 수 있습니다.
🔥 정렬 함수는 어떤 숫자든 반환할 수 있습니다.
정렬 함수의 반환 값엔 제약이 없습니다. 양수를 반환하는 경우 첫 번째 인자가 두 번째 인자보다 "크다"를 나타내고, 음수를 반환하는 경우 첫 번째 인자가 두 번째 인자보다 "작다"를 나타내기만 하면 됩니다.
이 점을 이용하면 정렬 함수를 더 간결하게 만들 수 있습니다.
let arr = [ 1, 2, 15 ];
// 오름차순
arr.sort( (a, b) => a - b );
alert(arr); // 1, 2, 15
// 내림차순
arr.sort( (a, b) => b - a );
alert(arr); // 15, 2, 1
🔥 문자열엔 localeCompare를 사용하세요.
Ö
같은 문자가 있는 언어에도 대응하려면 str.localeCompare
메서드를 사용해 문자열을 비교하는 게 좋습니다.
let countries = ["Österreich", "Andorra", "Vietnam"];
alert( countries.sort( (a, b) => a > b ? 1 : -1) ); // Andorra, Vietnam, Österreich (제대로 정렬이 되지 않았습니다.)
alert( countries.sort( (a, b) => a.localeCompare(b) ) ); // Andorra,Österreich,Vietnam (제대로 정렬되었네요!)
arr.reverse
는 arr
의 요소를 역순으로 정렬시켜주는 메서드입니다.
let arr = [1, 2, 3, 4, 5];
arr.reverse();
alert( arr ); // 5,4,3,2,1
반환 값은 재 정렬된 배열입니다.
str.split(delim)
을 이용하면 구분자(delimiter) delim
을 기준으로 문자열을 쪼개줍니다.
아래 예시에선 쉼표와 공백을 합친 문자열이 구분자로 사용되고 있습니다.
let names = "Bilbo, Gandalf, Nazgul";
let arr = names.split(", ");
for (let name of arr) {
alert( `${name}에게 보내는 메시지` ); // Bilbo에게 보내는 메시지
}
split
메서드는 두 번째 인자로 숫자를 받을 수 있습니다. 이 숫자는 배열의 길이를 제한해주므로 길이를 넘어서는 요소를 무시할 수 있습니다. 실무에서 자주 사용하는 기능은 아닙니다.
let arr = "Bilbo, Gandalf, Nazgul, Saruman".split(", ", 2);
alert(arr); // Bilbo, Gandalf
split(s)
의 s
를 빈 문자열로 지정하면 문자열을 글자 단위로 분리하여 배열로 반환합니다.
let str = "test";
alert( str.split("") ); // t,e,s,t
arr.join(glue)
은 split
과 반대 역할을 하는 메서드입니다. 인자 glue
를 접착제처럼 사용해 배열 요소를 모두 합친 후 하나의 문자열을 만들어줍니다.
let arr = ["Bilbo", "Gandalf", "Nazgul"];
let str = arr.join(";"); // 배열 요소 모두를 ;를 사용해 하나의 문자열로 합칩니다.
alert( str ); // Bilbo;Gandalf;Nazgul
reduce
와 reduceRight
는 배열을 기반으로 값 하나를 도출할 때 사용됩니다.
let value = arr.reduce(function(accumulator, item, index, array) {
// ...
}, [initial]);
인자로 넘겨주는 함수는 배열의 모든 요소를 대상으로 차례차례 적용되는데, 적용 결과는 다음 함수 호출 시 사용됩니다.
함수의 인자는 다음과 같습니다.
accumulator
이전 함수 호출의 결과.initial
은 함수 최초 호출 시 사용되는 초깃값을 나타냄(옵션)
item
현재 배열 요소
index
요소의 위치
array
배열
이전 함수 호출 결과는 다음 함수를 호출할 때 첫 번째 인자(previousValue)로 사용됩니다.
첫 번째 인자는 앞서 호출했던 함수들의 결과가 누적되어 저장되는 "누산기(accumulator)"라고 생각하면 됩니다. 마지막 함수까지 호출되면 이 값은 reduce
의 반환 값이 됩니다.
reduce
를 이용해 코드 한 줄로 배열의 모든 요소를 더한 값을 구해보겠습니다.
let arr = [1, 2, 3, 4, 5];
let result = arr.reduce((sum, current) => sum + current, 0);
alert(result); // 15
reduce
에 전달한 함수는 오직 인자 두 개만 받고 있습니다. 대개 이렇게 인자를 두 개만 받습니다.
이제 어떤 과정을 거쳐 위와 같은 결과가 나왔는지 자세히 살펴보겠습니다.
sum | current | result | |
---|---|---|---|
첫번째 호출 | 0 | 1 | 1 |
두번째 호출 | 1 | 2 | 3 |
세번째 호출 | 3 | 3 | 6 |
네번째 호출 | 6 | 4 | 10 |
다섯번째 호출 | 10 | 5 | 15 |
아래와 같이 초깃값을 생략하는 것도 가능합니다.
let arr = [1, 2, 3, 4, 5];
// reduce에서 초깃값을 제거함(0이 없음)
let result = arr.reduce((sum, current) => sum + current);
alert( result ); // 15
초깃값을 없애도 결과는 동일하네요. 초깃값이 없으면 reduce
는 배열의 첫 번째 요소를 초깃값으로 사용하고 두 번째 요소부터 함수를 호출하기 때문입니다.
하지만 이렇게 초깃값 없이 reduce
를 사용할 땐 극도의 주의를 기울여야 합니다. 배열이 비어있는 상태면 reduce
호출 시 에러가 발생하기 때문입니다.
let arr = [];
// TypeError: Reduce of empty array with no initial value
// 초깃값을 설정해 주었다면 초깃값이 반환되었을 겁니다.
arr.reduce((sum, current) => sum + current);
이런 예외상황 때문에 항상 초깃값을 명시해 줄 것을 권장합니다.
arr.reduceRight
는 reduce
와 동일한 기능을 하지만 배열의 오른쪽부터 연산을 수행한다는 점이 다른 메서드입니다.
자바스크립트에서 배열은 독립된 자료형으로 취급되지 않고 객체형에 속합니다.
따라서 typeof
로는 일반 객체와 배열을 구분할 수가 없죠.
alert(typeof {}); // object
alert(typeof []); // object
그런데 배열은 자주 사용되는 자료구조이기 때문에 배열인지 아닌지를 감별해내는 특별한 메서드가 있다면 아주 유용할 겁니다. Array.isArray(value)
는 이럴 때 사용할 수 있는 유용한 메서드입니다. value
가 배열이라면 true
를, 배열이 아니라면 false
를 반환해주죠.
alert(Array.isArray({})); // false
alert(Array.isArray([])); // true
함수를 호출하는 대부분의 배열 메서드(find
, filter
, map
등. sort
는 제외)는 thisArg
라는 매개변수를 옵션으로 받을 수 있습니다.
thisArg
는 아래와 같이 활용할 수 있습니다.
arr.find(func, thisArg);
arr.filter(func, thisArg);
arr.map(func, thisArg);
// ...
// thisArg는 선택적으로 사용할 수 있는 마지막 인자입니다.
thisArg
는 func
의 this
가 됩니다.
아래 예시에서 객체 army
의 메서드를 filter
의 인자로 넘겨주고 있는데, 이때 thisArg
는 canJoin
의 컨텍스트 정보를 넘겨줍니다.
let army = {
minAge: 18,
maxAge: 27,
canJoin(user) {
return user.age >= this.minAge && user.age < this.maxAge;
}
};
let users = [
{age: 16},
{age: 20},
{age: 23},
{age: 30}
];
// army.canJoin 호출 시 참을 반환해주는 user를 찾음
let soldiers = users.filter(army.canJoin, army);
alert(soldiers.length); // 2
alert(soldiers[0].age); // 20
alert(soldiers[1].age); // 23
thisArgs
에 army
를 지정하지 않고 단순히 users.filter(army.canJoin)
를 사용했다면 army.canJoin
은 단독 함수처럼 취급되고, 함수 본문 내 this
는 undefined
가 되어 에러가 발생했을 겁니다.
users.filter(user => army.canJoin(user))
를 사용하면 users.filter(army.canJoin, army)
를 대체할 수 있긴 한데 thisArg
를 사용하는 방식이 좀 더 이해하기 쉬우므로 더 자주 사용됩니다.