자바스크립트에서 가장 중요한 3가지를 꼽으라 하면 객체, 함수, 그리고 배열일 것이다. 가장 큰 이유 중에 하나는 JSON이 배열과 객체의 조합으로 이루어져 있다는 것.
하지만 자바스크립트의 배열은 자료구조에서 말하는 엄밀한 의미의 배열이 아니다. 자료구조에서 배열의 정의는 연속된 메모리의 집합이다. 특정 메모리의 주소값을 배열 변수의 값으로 가지고, (인덱스 * 자료형 크기) 를 1번의 연산으로 찾아갈 수 있다면 이를 배열이라고 한다. 한번의 연산으로 원하는 값을 찾는 것을 random access 라고 한다. 자바스크립트의 배열은 Array.prototype 객체를 상속받는 인스턴스이며 이 Array.prototype 은 Object.prototype을 상속받고 있다. 그러므로 자료구조에서 말하는 배열과는 다르며, 객체로 만들어져서 마치 배열과 같이 작동하기 때문에 배열이라고 부른다.
일반 객체와 배열을 구분하는 가장 명확한 차이는, 객체 내부의 프로퍼티에 순서의 의미가 있다는 것과, length 프로퍼티를 갖는다는 것이다. 그러므로 보통 배열은 인덱스를 가지고, length 프로퍼티를 가지는 객체를 칭한다.
자바스크립트의 배열은 객체이기 때문에 인덱스를 통해 random access를 하지 않는다. 하지만 해시테이블을 통해 객체를 구현하기 때문에 마찬가지로 한번의 해시 연산을 통해 원하는 데이터에 접근할 수 있다. O(1) 으로 데이터 접근을 할 수 있는건 같지만, 해시 연산의 시간만큼 느리다. 하지만 데이터 추가 삭제가 해시테이블의 경우 O(1) 인 것을 생각하면, 순수 배열보다 오히려 좋은 성능을 기대할 수 있을 것이다.
자바스크립트 엔진은 인덱스 접근이 느리다는걸 고려하여, 배열을 일반객체와 구분해서 메모리 상에서 좀 더 배열처럼 사용할 수 있도록 구현되어 있다. 배열이 일반 객체보다 약 2배 정도 빠르다.
배열을 이야기하면서 length 프로퍼티가 빠질 수 없다. 보통은 length 는 배열의 요소의 개수를 말하지만 자바스크립트에서 length 는 꼭 이와 같지는 않다. length는 배열에 요소가 추가되고 삭제될 때마다 자동으로 늘어나고 줄어들지만, 일반 프로퍼티와 같기 때문에, 직접 값을 할당할 수도 있다. 물론 이런 짓을 하는 건 좋지 않다. 하지만 가능하다는 것이 문제.
const arr = [1,2,3,4,5]
arr.length = 3
console.log(arr) // [1,2,3]
arr.length = 5
console.log(arr) // [1,2,3,empty * 2]
위의 예제는 length 프로퍼티에 값을 할당할 수 있고, 할당한다면 배열의 값이 바뀌는 것을 볼 수 있다. 저 empty는 값이 없다는 것을 의미한다. 값이 아니다. 이 empty 가 중간에 생길 수도 있다. 다른 언어의 배열은 메모리의 연속성은 없더라도 요소의 연속성은 강제하는데 자바스크립트는 그런것도 없다. [1,,3] 이렇게 선언하면 [1, empty,3] 바로 이렇게 만들어진다. 이런 length 와 실제 원소의 개수의 차이가 발생하는 배열을 희소 배열이라 한다.
자바스크립트에서 배열을 생성하는 방법은 네 가지가 있다.
간단하게 const a = [1,2,3] 이런식으로 배열을 선언하는 방식이다.
Array 생성자 함수는 전달되는 인수의 개수에 따라 다르게 동작한다.
const arr = new Array(10);
console.log(arr); // [empty * 10]
console.log(arr.length); 10
인수가 한 개일 경우 전달된 인수의 길이만큼의 희소배열을 만들어낸다.
new Array(1,2,3); // [1,2,3]
new Array({}); // [{}]
인수가 2개 이상이면 해당 인수를 요소로 갖는 배열을 생성한다.
또한 인수가 한 개여도 숫자가 아니라면 해당 인수를 요소로 갖는 배열을 생성한다.
ES6에서 도입되었고, 전달된 인수를 요소로 갖는 배열을 생성한다.
ES6에서 도입되었고, Array.from 메서드는 유사 배열 객체 또는 이터러블 객체를 인수로 전달받아 배열로 변환하여 반환한다.
유사 배열 객체는 인덱스로 프로퍼티 값에 접근할 수 있고, length 프로퍼티를 갖는 객체를 말한다. 유사 배열 객체와 배열의 가장 큰 차이는 Array.prototype을 상속을 받느냐 안받느냐는 것이다. 이를 통해 만들어지는 차이는 배열의 메서드를 사용할 수 있냐 없냐.
이터러블 객체는 나중에 정리하겠지만, Symbol.iterator 메서드를 구현하여, iterator 객체를 반환하는 객체를 말한다. for...of 문으로 순회할 수 있으모, 스프레드, 디스트럭쳐링 문법을 사용할 수 있는 객체이다.
Array.prototype 에 존재하는 메서드를 잘 알고 있어야 배열을 잘 쓸 수 있다.
배열에는 원본 배열을 직접 변경하는 메서드가 있고, 새로운 배열을 생성하여 반환하는 메서드가 있다. 이를 잘 구분하는 것이 중요하다.
Array 생성자 함수의 정적 메서드이다. 바로 호출하여 해당 객체가 배열인지 아닌지 확인할 수 있다.
원본 배열에서 인수로 전달된 요소를 검색하여 첫번째 인덱스를 반환한다. 요소가 존재하지 않으면 -1 을 반환한다.
인수로 받은 모든 값을 원본 배열에 마지막 요소로 추가해준다. 하지만 이 방법보단 스프레드 문법을 사용하는 것이 좋다.
원본 배열에서 마지막 요소를 제거한다.
원본 배열의 앞에 값을 넣어준다. 역시 스프레드 문법을 사용하는 것이 좋다.
인수로 전달된 값을 원본 배열에 마지막 값으로 추가한 새로운 배열을 반환한다. 인수로 전달된 값이 배열인 경우 해체하여 추가된다. concat 보단 스프레드를 사용하는 것이 통일성이 있어 좋다.
원본 배열의 중간 요소를 추가하거나 제거한다. 원본 배열을 직접 변경.
인수로 전달된 범위의 요소들을 복사하여 새로운 배열로 반환한다. start, end의 인수를 가져서 인덱스를 통해 범위를 지정해준다. 이런 특성을 이용해 유사배열객체를 배열로 변환해줄 수도 있다. 하지만 Array.from 이 있는데 굳이 이런 방법을 쓸 이유는 없다.
원본 배열의 모든 요소를 문자열로 변환한 후 구분자로 연결해 만들어진 문자열을 반환한다.
원본 배열을 뒤집는다.
배열을 모두 전달받은 인수로 채운다. 인수에 start, end 인덱스를 지정할 수 있다.
ES7 에서 도입되었으며, 특정 요소가 포함되어있는지 확인하여, true, false를 반환한다. indexOf의 거추장스러운 부분을 개선해준다.
배열 내에 배열이 있을 경우, 해당 배열을 1차원 배열로 변환한다. 인수를 통해 얼마나 깊은 배열까지 평탄화 시킬지 결정할 수 있다.
배열 메서드이지만, 콜백함수를 인수로 받는 메서드를 말한다. 특히 고차함수는 순수 함수를 지향하기 때문에, 함수형으로 프로그램을 작성할 때 특히 많이 사용하게 된다.
원본 배열을 정렬한다. 숫자 요소를 정렬할 때 문제가 있는데, 기본적으로 sort 메서드는 유니코드의 순서를 따르기 때문에, 숫자의 경우 [1, 10, 100, 2, 20, 200] 이런 식으로 정렬이 되게 된다. 때문에, 숫자를 정렬할 경우 추가적으로 콜백함수를 인수로 넣어줘서, 다르게 작동하게 해주어야 한다.
function compare(key){
return (a, b) => (a[key] > b[key] ? 1 : (a[key] < b[key] ? -1 :0));
}
arr.sort(compare("기준"));
함수형 프로그래밍은 순수함수와 보조함수의 조합을 통해 로직 내에 존재하는 조건문과 반복문을 제거하여 복잡성을 해결하고 변수의 사용을 억제하여 상태 변경을 피하려는 프로그래밍 패러다임이다. forEach 는 for 문을 대체하는 고차함수이다.
forEach 메서드의 콜백함수는 3개의 인수를 전달한다. 요소값, 인덱스, this(forEach 를 호출한 배열). 기본적으로 forEach는 원본 배열을 변경하지는 않지만, this를 통해 변경할 수 있다. 그리고 반환값이 undefined이다. 이 때문에 실질적으로 forEach를 통해 배열에 변경을 주려면 원본 배열을 변경시킬 수 밖에 없다.
이 부분에서 조심해야 되는 것이 콜백함수는 forEach 내부에서 일반 함수로 호출되기 때문에, 콜백 함수 내부에 this를 썼다면 this가 전역객체와 바인딩 되게 된다. 이를 해결하기 위해, 콜백함수에 this를 명시적으로 전달하는 것을 통해 forEach를 호출한 배열과 바인딩 시켜줄 수 있다. 더 나은 방법은 화살표 함수를 사용하면 자동적으로 forEach를 호출한 배열의 this를 찾아가므로 더 자연스러운 방법이다.
forEach는 폴리필을 알아보는 것이 동작 이해에 도움이 된다.
if (!Array.prototype.forEach){
Array.prototype.forEach = function(callback, thisArg){
if(typeof callback !== "function"){
throw new TypeError(callback + ' is not a function');
}
thisArg = thisArg || window; // this 전달이 안되면 전역객체에 연결
for(var i = 0; i < this.length; i++){
callback.call(thisArg, this[i], i, this);
// 함수로 전달되는 this 는 해당 forEach 메서드를 호출한 배열이다.
// 그러므로 콜백함수에 전달되는 this 인수는 배열임을 보장할 수 있다.
}
}
}
forEach는 성능은 안 좋지만 (break, continue가 안되서) 가독성은 더 좋다.
자신을 호출한 배열의 모든 요소를 순회하면서 인수로 전달받은 콜백함수를 호출한다. 그리고 이 콜백함수의 반환값으로 구성된 새로운 배열을 리턴한다.
forEach 와 map의 공통점은 자신을 호출한 배열의 모든 요소를 순회하면서 콜백함수를 반복 호출하는 것이다. 반면 forEach는 undefined를 리턴하고 map은 콜백함수의 반환값으로 구성된 새로운 배열을 리턴하는 차이가 있다.
즉 forEach 메서드는 단순히 반복문을 대체하기 위한 고차함수이고, map 메서드는 요소값을 다른 값으로 매핑한 새로운 배열을 만들기 위한 함수이다.
[1,2,3].map((item, index, arr) => {
console.log(`요소값: ${item} 인덱스: ${index}, this: ${arr}`);
}
자신이 호출한 배열의 모든 요소를 순회하면서 인수로 전달받은 콜백함수의 반환값이 true인 요소들로만 구성된 새로운 배열을 반환한다.
filter 메서드는 호출한 배열을 검사하여 범위를 줄여가기 때문에 반드시 기존 배열보다 작거나 같은 배열이 반환된다.
console.log([1,2,3].filter(number => number === 1)); // [1]
게시판의 글 목록 중, 특정 키에 맞는 글만 보여주려고 하거나, 배열을 필터해야 할 때 사용된다.
배열의 모든 요소를 순회하며, 콜백함수의 반환값을 다음 번에 실행될 콜백함수의 인수로 전달한다. 즉 함수의 결과값을 누적해간다.
reduce 메서드는 콜백 함수와 초기값을 인수로 전달받는다. 콜백 함수에는 초기값(이전반환값), 요소값, 인덱스, 메서드를 호출한 배열이 인수로 전달된다.
const sum = [1,2,3,4].reduce((accumulator, cValue, index, array) => accumulator + cValue, 0);
위의 코드를 실행하면, 배열의 모든 합이 리턴된다.
그러므로 reduce 함수는 반드시 한개의 값만이 리턴된다.
map, filter, reduce 중에 무엇을 써야 될지 감이 안 올 경우, 내가 만들려는 함수가 몇개를 반환하는지 생각해보면 좋다.
배열을 순회하면서 인수로 전달한 콜백함수의 반환값이 한번이라도 참이면 true를 반환하고 모두 거짓일 때 false를 반환한다.
배열을 순회하면서 인수로 전달한 콜백함수의 반환값이 한번이라도 거짓이면 false를 반환하고 모두 참일 때 true를 반환한다.
배열을 순회하면서 인수로 전달한 콜백함수의 반환값이 참인 첫번째 요소를 반환한다.
배열을 순회하면서 인수로 전달한 콜백함수의 반환값이 참인 첫번째 요소의 인덱스를 반환한다.
자바스크립트에서 배열은 정말 많이 사용되는 자료구조 중 하나이다. 때문에 언제 어디서 메서드를 사용할지 모르기 때문에, 외우지는 못하더라도, 어떤 메서드가 있고, 이름이 무엇인지는 알아두어야 필요할 때 찾아서 사용할 수 있다.