Array
배열은 리스트와 비슷한 '객체'이다. JS의 배열은 길이도 고정되어 있지 않고, 요소의 자료형도 고정되어 있지 않다. 따라서 한 배열에 숫자와 문자열이 같이 들어갈 수도 있고, 아래와 같이 그냥 뒤에 값을 넣을 수도 있다.
let arr = [1, 2];
arr[2] = 3;
console.log(arr); // [1, 2, 3]
또한 위와 같이 그냥 리터럴 표기법으로 배열을 생성할 수도 있고, 생성자로 생성할 수 있다. 이제 아래에서 생성자를 시작으로 관련 메소드들을 알아보자.
Array() 생성자
let arr1 = new Array(3);
console.log(arr1); // [empty * 3]
console.log(arr1[0]); // undefined
위 코드와 같이 '빈 슬롯' 3개를 가진 Array 객체를 생성한다. 위 코드에서 arr의 0번째 element를 출력해보았다.
undefined를 출력하지만, arrayLength 만큼의 빈 슬롯을 가지는것이고 실제로 undefined가 채워져 있지 않다.
여기서 아래와 같이 생성자에 인자를 하나 더 주게되면, 즉 인자를 2개 이상 주게 되면 인자로 받은 값들로 배열을 생성한다. 또한 인자를 하나만 주더라도 숫자가 아닌 문자열이라면 그 값은 그대로 배열에 들어가게 된다.
let arr1 = new Array(3, 1);
console.log(arr1); // [3, 1]
console.log(arr1[0]); // 3
let arr2 = new Array('hello');
console.log(arr2); // ['hello']
Array.prototype.at()
let arr1 = [5, 12, 8, 130, 44];
console.log(arr1[3]); // 130
console.log(arr1.at(3)); // 130
console.log(arr1[arr1.length - 1]); // 44
console.log(arr1.at(-1)); // 44
console.log(arr1.at(arr1.length - 1)); // 44
기존에 사용하던 대괄호로 배열의 값에 접근하는것과 크게 다를 것이 없다. 그런데 굳이 at()을 써야 하는 이유는 뭘까? 위 코드를 보면 index가 3인 값에 접근할 때 위와 같은 두가지 방법 모두 130을 출력한다.
그런데 우리가 만약 마지막 값에 대해 접근해야 한다고 생각해보자. 물론 위 코드의 경우 배열의 길이가 한눈에 보여서 마지막 index가 4라는 것을 알 수 있다. 따라서 아래 코드를 보면 위의 코드와 똑같이 접근할 수 있다.
let arr1 = [5, 12, 8, 130, 44];
console.log(arr1[4]); // 44
console.log(arr1.at(4)); // 44
그런데 만약 배열의 길이가 너무 길거나, 길이를 알 수 없는 경우에는 어떻게 접근해야 할까? 보통은 아래와 같이 접근한다.
let arr1 = [5, 12, 8, 130, 44];
console.log(arr1[arr1.length - 1]); // 44
console.log(arr1.at(arr1.length - 1)); // 44
.length로 배열의 길이를 구해서 마지막 index에 접근하여 배열의 마지막 값을 알아낸다. .at()으로도 똑같이 구할 수 있다.
하지만 여기서 .at()만 사용할 수 있는 배열의 마지막 값에 접근하는 것 때문에 이걸 쓴다고 생각한다 ㅎ....
console.log(arr1.at(-1)); // 44
마지 파이썬에서 처럼 .at(-1)을 하게되면 길게 쓸 필요없이 마지막 값에 접근할 수 있다. 이것 외에도 개인적으로 깔끔하게 보여서 .at()을 자주 사용한다!
Array.prototype.concat()
const array1 = ['a', 'b', 'c'];
const array2 = ['d', 'e', 'f'];
const array3 = array1.concat(array2);
console.log(array3);
// Expected output: Array ["a", "b", "c", "d", "e", "f"]
배열을 이어붙여주는 메소드이다. 기존 배열의 값을 변경하지 않고 이어붙인 새로운 배열을 반환한다.
하지만 나라면 위 방법을 쓰는대신에 아래와 같이 쓰련다...
const array1 = ['a', 'b', 'c'];
const array2 = ['d', 'e', 'f'];
const array3 = [...array1, ...array2];
console.log(array3);
// Expected output: Array ["a", "b", "c", "d", "e", "f"]
위와 같이 전개연산자(spread operator)를 이용하면 보기 쉽게 이어붙일 수도 있다. 배열 뿐만 아니라 뒤에 값을 그대로 써줘도 값을 이어붙일 수 있다.
Array.prototype.every()
let arr1 = [100, 10, 3, 4];
console.log(arr1.every((el) => el > 0)); // true
let arr2 = [100, -10, 3, 4];
console.log(arr2.every((el) => el > 0)); // false
console.log(arr2.every((el) => {
return el > 0;
})); // false
배열안의 모든 값이 주어진 콜백 함수를 만족하는지 판단하는 메소드이다. 간단한 한줄의 콜백함수일 경우 arr1에서 every()를 사용한 것과 같이 간단하게 사용할 수 있다. 물론 복잡한 함수일 경우 위처럼 인라인으로 작성하지 않고 따로 함수로 빼서 작성한 후에 콜백함수 자리에 넣어줄 수 있다.
.every() 메소드는 주어진 콜백함수에 대해 거짓을 판단하게 되면 순회를 중지하고 false를 반환한다.
그리고 위에서 '빈 슬롯'이 나와서 하는 말인데 아마 다른 메소드들도 똑같겠지만 빈 슬롯에 대해서는 판단하지 않는다.
let arr3 = [100, 10, , 3, 4];
console.log(arr3.every((el) => el > 0)); // true
10과 3중간에 빈 슬롯이 있지만 true를 반환한다.
Array.prototype.filter()
let arr2 = [100, -10, 3, ,4];
let result = arr2.filter((el) => el > 0);
console.log(result); // [100, 3, 4]
result = arr2.filter((el) => {
return el > 0;
});
console.log(result); // [100, 3, 4]
주어진 함수의 테스트를 통과하는 모든 요소를 모아 새로운 배열로 반환한다.
위에서도 설명했다시피 한줄은 그냥 간단하게 쓸 수 있고, 그렇지 않다면 안에 익명함수로 직접 작성 할 수도 있다. 물론 외부에 작성한 함수를 넣어줘도 된다. 또한 역시 빈 슬롯은 판단자체를 하지않는다.
Array.prototype.forEach()
let arr = [100, -10, 3, ,4];
arr.forEach((el, idx) => {
console.log(el, idx);
});
/*
100 0
-10 1
3 2
4 3
*/
주어진 함수를 배열 요소 각각에 대해 실행한다. .map()과 달리 배열의 값을 바꾸지 않기 때문에 나는 거의 쓰는일이 없었던 것 같다. 물론 콜백함수가 값을 바꿀 수는 있다. 하지만 for...of를 주로 애용한 것 같다. 심지어 forEach()는 중간에 반복문을 멈출 수 없다. 정말 모든 요소에 대해서 단순 반복적인 어떤 일을 실행해야 할 때 사용하면 좋은 것 같다.
Array.from()
유사 배열 객체(array-like object)나 반복 가능한 객체(iterable object)를 얕게 복사해 새로운Array 객체를 만든다.
여러 객체들을 Array 객체로 만들 수 있기 때문에 특히 PS 할 때 자주 사용하는 메소드이다.
console.log(Array.from('foo'));
// Expected output: Array ["f", "o", "o"]
console.log(Array.from([1, 2, 3], x => x + x));
// Expected output: Array [2, 4, 6]
위와 같이 문자열을 인자로 주면 분리해서 Array 객체로 만들어지고, 값에 대한 조작도 가능하다.
나는 이중 배열을 만들 때 아래와 같이 자주 사용했던 것 같다.
let arr = Array.from({ length: 5 }, () => Array(5).fill(0));
위와 같이 하면 5 * 5의 0으로 채워진 배열이 만들어진다.
또 다른 예시로 가끔 사용했던 1,2,3,4,5와 같은 배열로 초기화 할 때이다.
let arr = Array.from({ length: 5 }, (_, idx) => idx + 1);
console.log(arr); // [1, 2, 3, 4, 5];
꼭 위와 같은 예시가 아니더라도 패턴을 가진채로 초기화 하는게 되게 유용한 것 같다.
Array.prototype.includes()
배열이 특정 요소를 포함하고 있는지 판별한다.
const array1 = [1, 2, 3];
console.log(array1.includes(2));
// Expected output: true
const pets = ['cat', 'dog', 'bat'];
console.log(pets.includes('cat'));
// Expected output: true
console.log(pets.includes('at'));
// Expected output: false
Array.prototype.indexOf()
배열에서 지정된 요소를 찾을 수 있는 첫 번째 인덱스를 반환하고 존재하지 않으면 -1을 반환한다.
const beasts = ['ant', 'bison', 'camel', 'duck', 'bison'];
console.log(beasts.indexOf('bison'));
// Expected output: 1
// Start from index 2
console.log(beasts.indexOf('bison', 2));
// Expected output: 4
console.log(beasts.indexOf('giraffe'));
// Expected output: -1
바로 위에 언급한 .includes() 대신 .indexOf()를 이용하여 아래와 같이 판단할 수도 있다.
let arr = [1, 2, 3, 4, 5];
console.log(arr.indexOf(6) > -1);
찾지 못하면 -1을 반환하므로 -1보다 크다면 무조건 배열 안에 있다는 뜻이 된다. 따라서 이와 같이 안에 요소가 있는지 판별할 수도 있다.
Array.prototype.join()
배열의 모든 요소를 연결해 하나의 문자열로 만든다.
const elements = ['Fire', 'Air', 'Water'];
console.log(elements.join());
// Expected output: "Fire,Air,Water"
console.log(elements.join(''));
// Expected output: "FireAirWater"
console.log(elements.join('-'));
// Expected output: "Fire-Air-Water"
위와 같이 구분자를 인자로 넣어 연결할 요소들 사이사이에 구분자를 넣어줄 수 있다.
let arr = ['hi', undefined, 2, 3, null, 4, 5];
console.log(arr.join('')); // hi2345
위와 같이 undefined와 null은 빈 문자열로 넘겨준다.
Array.prototype.map()
배열 내의 모든 요소 각각에 대하여 주어진 함수를 호출한 결과를 모아 새로운 배열을 반환한다.
사실상 React를 사용한다면 제일 많이 사용하는 메소드가 아닐까 생각이든다!
PS할때도 map과 reduce를 많이 사용하는데, map이 가장 범용성이 큰 것 같다.
const array1 = [1, 4, 9, 16];
// Pass a function to map
const map1 = array1.map(x => x * 2);
console.log(map1);
// Expected output: Array [2, 8, 18, 32]
보통 API로 통신하다보면 숫자 데이터를 받아올거라 생각했음에도 모든 요소가 문자열 형태로 올 때가 있다.
예를들어 ['1', '2', '3', '4', ... ] 와 같이 말이다.
그럴 때 map을 이용해서 정말 자주 사용하는 방법이 아래와 같은 방법이다.
let arr = ['1', '2', '3', '4', '5'];
console.log(arr); // ['1', '2', '3', '4', '5']
arr = arr.map(Number);
console.log(arr); // [1, 2, 3, 4, 5]
일단 Number()는 인자로 하나만 받는다. 그리고 그 받은 인자를 숫자로 변환하여 반환해준다. 그리고 map에서 순환하는 값들이 Number()에 인자로 들어간다. 위에서 계속 봤듯이 콜백함수는 간단할경우 줄여서 사용할 수 있다. 이것도 이와 같은 방법이 아닐까 생각한다.
그런데 Number()로 손쉽게 변환이 가능하므로 parseInt()도 가능할 것 같다.
let arr = ['1', '2', '3', '4', '5'];
console.log(arr); // ['1', '2', '3', '4', '5']
arr = arr.map(parseInt);
console.log(arr); // [1, NaN, NaN, NaN, NaN]
하지만 위와 같은 결과를 반환한다. 이유는 무엇일까?
parseInt()는 인자를 하나만 주면 기본적으로 10진수로 변환한다. 하지만 인자를 하나 더 줄경우에는 다른 진수로 반환하게 된다. 즉, 인자가 무조건 하나인것이 아니라 두개를 줄 수도 있기 때문에 위와 같이 제대로 처리가 되지 않는것이다.
따라서 parseInt를 통해 제대로 처리하려면 아래와 같이 해주어야 한다.
let arr = ['1', '2', '3', '4', '5'];
console.log(arr); // ['1', '2', '3', '4', '5']
arr = arr.map(el => parseInt(el));
console.log(arr); // [1, 2, 3, 4, 5]
Array.prototype.pop()
배열에서 마지막 요소를 제거하고 그 요소를 반환한다.
var myFish = ['angel', 'clown', 'mandarin', 'sturgeon'];
var popped = myFish.pop();
console.log(myFish); // ['angel', 'clown', 'mandarin' ]
console.log(popped); // 'sturgeon'
만약 배열이 비어있다면 undefined를 반환한다.
Array.prototype.push()
배열의 끝에 하나 이상의 요소를 추가하고, 배열의 새로운 길이를 반환한다.
const animals = ['pigs', 'goats', 'sheep'];
const count = animals.push('cows');
console.log(count);
// Expected output: 4
console.log(animals);
// Expected output: Array ["pigs", "goats", "sheep", "cows"]
animals.push('chickens', 'cats', 'dogs');
console.log(animals);
// Expected output: Array ["pigs", "goats", "sheep", "cows", "chickens", "cats", "dogs"]
음... 물론 push라는 단어가 무엇을 하는지 정확히 알 수 있게 해주어서 좋지만, 전개 연산자가 나온뒤로 아래와 같은 방법이 더 편하다고 생각한다.
let arr = [1, 2, 3];
arr = [...arr, 4];
console.log(arr); // [1, 2, 3, 4]
Array.prototype.reduce()
배열의 각 요소에 대해 주어진 리듀서 (reducer) 함수를 실행하고, 하나의 결과값을 반환한다.
사실 가장 많이 사용하는 예시는 아래와 같이 누적합을 구할 때, 혹은 배열의 총 합을 구할 때 가장 많이 사용된다.
let arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let sum = arr.reduce((acc, cur) => acc + cur);
console.log(sum); // 55
위의 예시에서는 마지막 인자를 주지 않아서 자동으로 배열의 맨 첫번째 값인 1이 초기값이 되었다. 하지만 아래와 같이 마지막 인자로 초기값을 줄 수도 있다.
let arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let sum = arr.reduce((acc, cur) => acc + cur, 10);
console.log(sum); // 65
개인적으로 가장 활용도가 높고 뭔가 사용하기 어려운 메소드라고 생각한다. 신기한 예시가 있다면 추후에 추가해봐야겠다...
Array.prototype.shift()
배열에서 첫 번째 요소를 제거하고, 제거된 요소를 반환한다. 이 메서드는 배열의 길이를 변하게 한다.
const array1 = [1, 2, 3];
const firstElement = array1.shift();
console.log(array1);
// Expected output: Array [2, 3]
console.log(firstElement);
// Expected output: 1
.pop() 메소드와 함께 사용한다면 queue의 자료구조 형태를 지니게 할 수 있다. queue는 FIFO(First-In-First-Out) 형태기 때문에 배열에 가장 먼저 들어간 요소인 맨 앞을 가장 먼저 꺼낼 수 있다.
하지만 PS(Problem Solving) 관점에서 본다면 shift()를 사용할 경우 뒤의 요소들을 한 칸씩 앞으로 당겨야 하므로 시간적인 측면에서 불리할 수 있다.
Array.prototype.slice()
어떤 배열의 begin 부터 end 까지(end 미포함)에 대한 얕은 복사본을 새로운 배열 객체로 반환한다. 원본 배열은 바뀌지 않는다.
const animals = ['ant', 'bison', 'camel', 'duck', 'elephant'];
console.log(animals.slice(2));
// Expected output: Array ["camel", "duck", "elephant"]
console.log(animals.slice(2, 4));
// Expected output: Array ["camel", "duck"]
console.log(animals.slice(1, 5));
// Expected output: Array ["bison", "camel", "duck", "elephant"]
console.log(animals.slice(-2));
// Expected output: Array ["duck", "elephant"]
console.log(animals.slice(2, -1));
// Expected output: Array ["camel", "duck"]
console.log(animals.slice());
// Expected output: Array ["ant", "bison", "camel", "duck", "elephant"]
Array.prototype.some()
배열 안의 어떤 요소라도 주어진 판별 함수를 적어도 하나라도 통과하는지 테스트한다. 만약 배열에서 주어진 함수가 true을 반환하면 true를 반환한다. 그렇지 않으면 false를 반환한다. 이 메서드는 배열을 변경하지 않는다.
const array = [1, 2, 3, 4, 5];
// Checks whether an element is even
const even = (element) => element % 2 === 0;
console.log(array.some(even));
// Expected output: true
하나라도 거짓이면 즉시 순회를 멈추고 false를 반환했던 .every()와 반대되는 느낌의 메소드이다.
이 메소드 역시 주어진 콜백함수에 대해 truly한 값을 반환받는다면 그 즉시 순환을 멈추고 truly한 값을 반환한다.
Array.prototype.sort()
배열의 요소를 적절한 위치에 정렬한 후 그 배열을 반환한다. 정렬은 stable sort가 아닐 수 있다. 기본 정렬 순서는 문자열의 유니코드 코드 포인트를 따른다.
const array1 = [1, 30, 4, 21, 100000];
array1.sort();
console.log(array1);
// Expected output: Array [1, 100000, 21, 30, 4]
위의 예시를 보면 숫자의 크기대로 정렬이 잘 되지 않는다. 왜냐하면 유니코드의 코드 포인트를 따르기 때문이다.
쉽게 말하면 숫자로 보는것이 아닌 아스키코드로 보면 된다고 생각하면 될 것 같다. 100000의 맨 첫자리인 1과 21의 맨 첫자리인 2를 비교했을때 1이 더 작으므로 100000이 더 앞에 오게 되는 것이다.
따라서 제대로 정렬하려면 아래와 같이 바꿔주어야 한다.
let arr1 = [1, 30, 4, 21, 100000];
arr1.sort((a, b) => a - b);
console.log(arr1); // [1, 4, 21, 30, 100000]
let arr2 = [1, 30, 4, 21, 100000];
arr2.sort((a, b) => b - a);
console.log(arr2); // [100000, 30, 21, 4, 1];
콜백함수의 인자로 주어지는 a, b는 정렬할 두 수이다. 그리고 a - b를 실행하게 된다. arr1의 sort에서 보면 맨 처음에 정렬을 하기위해 1 - 30을 실행하게 된다. 그러면 -29로 음수가 나온다. 음수면 자리를 바꾸지 않고 그대로 있게한다.
다른 값들에 대해서도 양수면 자리를 바꾸고, 음수는 자리를 바꾸지 않는다. 이것을 이용해 오름차순과 내림차순을 간단히 구현할 수 있다.
원본배열을 바꾸게 되므로 주의해야 한다!!!
Array.prototype.splice()
배열의 기존 요소를 삭제 또는 교체하거나 새 요소를 추가하여 배열의 내용을 변경한다.
const months = ['Jan', 'March', 'April', 'June'];
months.splice(1, 0, 'Feb');
// Inserts at index 1
console.log(months);
// Expected output: Array ["Jan", "Feb", "March", "April", "June"]
months.splice(4, 1, 'May');
// Replaces 1 element at index 4
console.log(months);
// Expected output: Array ["Jan", "Feb", "March", "April", "May"]
slice와 비슷하지만 원본 배열을 건드리는데에 차이가 있다.
그리고 보통 splice는 배열에서 범위를 지정해서 삭제할 때 많이 쓰인다.
var myFish = ['parrot', 'anemone', 'blue', 'trumpet', 'sturgeon'];
var removed = myFish.splice(myFish.length - 3, 2);
// myFish is ["parrot", "anemone", "sturgeon"]
// removed is ["blue", "trumpet"]
이렇게 삭제할시에 splice는 삭제한 배열을 반환하게 된다.
'Web > Vanilla JS' 카테고리의 다른 글
얕은 복사와 깊은 복사(Shallow Copy & Deep Copy) (0) | 2023.04.03 |
---|---|
Object Method (0) | 2023.03.30 |
Modern Javascript / 함수 심화학습 (0) | 2021.07.08 |
Modern Javascript / 코드 품질 - Chrome으로 디버깅 (0) | 2021.07.07 |