배열 역시 객체형에 속하므로 프로퍼티를 지울 때 쓰는 연산자 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)는 만능 스위스 맥가이버 칼 같은 메서드입니다. 요소를 자유자재로 다룰 수 있게 해주죠. 이 메서드를 사용하면 요소 추가, 삭제, 교체가 모두 가능합니다.
첫 번째 매개변수는 조작을 가할 첫 번째 요소를 가리키는 인덱스(index)입니다. 두 번째 매개변수는 deleteCount로, 제거하고자 하는 요소의 개수를 나타냅니다. elem1, ..., elemN은 배열에 추가할 요소를 나타냅니다.
splice 메서드를 사용해 작성된 예시 몇 가지를 보여드리겠습니다.
먼저 요소 삭제에 관한 예시부터 살펴보겠습니다.
let arr = ["I", "study", "JavaScript"];
arr.splice(1, 1); // 인덱스 1부터 요소 한 개를 제거
alert( arr ); // ["I", "JavaScript"]
쉽죠? 인덱스 1이 가리키는 요소부터 시작해 요소 한 개(1)를 지웠습니다.
다음 예시에선 요소 세 개(3)를 지우고, 그 자리를 다른 요소 두 개로 교체해 보도록 하겠습니다.
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"
음수 인덱스도 사용할 수 있습니다.
slice 메서드 뿐만 아니라 배열 관련 메서드엔 음수 인덱스를 사용할 수 있습니다. 이때 마이너스 부호 앞의 숫자는 배열 끝에서부터 센 요소 위치를 나타냅니다. 아래와 같이 말이죠.
let arr = [1, 2, 5];
// 인덱스 -1부터 (배열 끝에서부터 첫 번째 요소)
// 0개의 요소를 삭제하고
// 3과 4를 추가합니다.
arr.splice(-1, 0, 3, 4);
alert( arr ); // 1,2,3,4,5
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은 이 객체를 배열처럼 취급합니다. 따라서 객체 전체가 아닌 객체 프로퍼티의 값이 더해집니다.
재정렬 후 배열 요소가 1, 15, 2가 되었습니다. 기대하던 결과(1, 2, 15)와는 다르네요. 왜 이런 결과가 나왔을까요?
요소는 문자열로 취급되어 재 정렬되기 때문입니다.
모든 요소는 문자형으로 변환된 이후에 재 정렬됩니다. 앞서 배웠듯이 문자열 비교는 사전편집 순으로 진행되기 때문에 2는 15보다 큰 값으로 취급됩니다("2" > "15").
기본 정렬 기준 대신 새로운 정렬 기준을 만들려면 arr.sort()에 새로운 함수를 넘겨줘야 합니다.
인수로 넘겨주는 함수는 반드시 값 두 개를 비교해야 하고 반환 값도 있어야 합니다.
function compare(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(function(a, b) { return a - b; });
alert(arr); // 1, 2, 15
strings에서 배운 비교 알고리즘이 기억나시나요? 이 알고리즘은 유니코드를 기준으로 글자를 비교합니다.
Ö같은 문자가 있는 언어에도 대응하려면 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 (제대로 정렬되었네요!)
메시지 전송 애플리케이션을 만들고 있다고 가정해 봅시다. 수신자가 여러 명일 경우, 발신자는 쉼표로 각 수신자를 구분할 겁니다. John, Pete, Mary같이 말이죠. 개발자는 긴 문자열 형태의 수신자 리스트를 배열 형태로 전환해 처리하고 싶을 겁니다. 입력받은 문자열을 어떻게 배열로 바꿀 수 있을까요?
str.split(delim)을 이용하면 우리가 원하는 것을 정확히 할 수 있습니다. 이 메서드는 구분자(delimiter) delim을 기준으로 문자열을 쪼개줍니다.
아래 예시에선 쉼표와 공백을 합친 문자열이 구분자로 사용되고 있습니다.
let names = 'Bilbo, Gandalf, Nazgul';
let arr = names.split(', ');
for (let name of arr) {
alert( `${name}에게 보내는 메시지` ); // Bilbo에게 보내는 메시지
}
split 메서드는 두 번째 인수로 숫자를 받을 수 있습니다. 이 숫자는 배열의 길이를 제한해주므로 길이를 넘어서는 요소를 무시할 수 있습니다. 실무에서 자주 사용하는 기능은 아닙니다.
그런데 배열은 자주 사용되는 자료구조이기 때문에 배열인지 아닌지를 감별해내는 특별한 메서드가 있다면 아주 유용할 겁니다. Array.isArray(value)는 이럴 때 사용할 수 있는 유용한 메서드입니다. value가 배열이라면 true를, 배열이 아니라면 false를 반환해주죠.
두 메서드는 map과 유사하게 모든 요소를 대상으로 함수를 호출합니다. some은 함수의 반환 값을 true로 만드는 요소가 하나라도 있는지 여부를 확인하고 every는 모든 요소가 함수의 반환 값을 true로 만드는지 여부를 확인합니다. 두 메서드 모두 조건을 충족하면 true를, 그렇지 않으면 false를 반환합니다.
배워야 할 메서드 종류가 너무 많아서 이걸 다 외워야 하나라는 생각이 들 수 있는데, 생각보다 쉬우니 너무 걱정하지 않으셨으면 좋겠습니다.
일단은 요약본을 참고해 자주 사용하는 메서드가 무엇인지 정도만 알아두어도 괜찮습니다. 아래 과제를 풀면서 충분히 연습하다 보면 배열 메서드에 대한 경험치가 쌓일 겁니다.
나중에 배열을 이용해 뭔가를 해야 하는데 방법이 떠오르지 않을 때 이곳으로 돌아와 요약본을 다시 보고 상황에 맞는 메서드를 찾으면 됩니다. 설명에 딸린 예시들이 실제 코드 작성 시 도움이 될 겁니다. 이런 과정을 반복하다 보면 특별한 노력 없이도 메서드를 저절로 외울 수 있습니다.
function camelize(str) {
return str
.split('-') // splits 'my-long-word' into array ['my', 'long', 'word']
.map(
// capitalizes first letters of all array items except the first one
// converts ['my', 'long', 'word'] into ['my', 'Long', 'Word']
(word, index) => index == 0 ? word : word[0].toUpperCase() + word.slice(1)
)
.join(''); // joins ['my', 'Long', 'Word'] into 'myLongWord'
}
function filterRangeInPlace(arr, a, b) {
for (let i = 0; i < arr.length; i++) {
let val = arr[i];
// 범위 밖의 요소를 제거함
if (val < a || val > b) {
arr.splice(i, 1);
i--;
}
}
}
let arr = [5, 3, 8, 1];
filterRangeInPlace(arr, 1, 4); // 1과 4 사이에 있지 않은 요소는 모두 제거함
alert( arr ); // [3, 1]
기능을 "확장"할 수 있는 계산기 객체를 만들어 주는 생성자 함수 Calculator를 작성해봅시다.
Calculator는 두 단계를 거쳐 만들 수 있습니다.
첫 번째 단계는 "1 + 2"와 같은 문자열을 받아서 “숫자 연산자 숫자” 형태(공백으로 구분)로 바꿔주는 메서드 calculate(str)를 구현하는 것입니다. 이 함수는 +와 -를 처리할 수 있어야 하고, 연산 결과를 반환해야 합니다.
예시:
let calc = new Calculator;
alert( calc.calculate("3 + 7") ); // 10
두 번째 단계는 계산기가 새로운 연산을 학습할 수 있도록 해주는 메서드 addMethod(name, func)를 추가해 주는 것입니다. 연산자 이름을 나타내는 name과 인수가 두개인 익명 함수 func(a,b)를 받는 새 메서드를 구현해야 하죠.
구현된 메서드를 이용해 곱셈 *과 나눗셈 /, 거듭제곱 **연산자를 추가해주는 예시는 아래와 같습니다.
let powerCalc = new Calculator;
powerCalc.addMethod("*", (a, b) => a * b);
powerCalc.addMethod("/", (a, b) => a / b);
powerCalc.addMethod("**", (a, b) => a ** b);
let result = powerCalc.calculate("2 ** 3");
alert( result ); // 8
name을 나타내는 프로퍼티를 가진 객체 user가 담긴 배열이 있습니다. name의 값만 담은 새로운 배열을 만들어주는 코드를 작성해보세요.
예시:
let john = { name: "John", age: 25 };
let pete = { name: "Pete", age: 30 };
let mary = { name: "Mary", age: 28 };
let users = [ john, pete, mary ];
let names = /* 여기에 코드를 작성하세요. */
alert( names ); // John, Pete, Mary
let john = { name: "John", age: 25 };
let pete = { name: "Pete", age: 30 };
let mary = { name: "Mary", age: 28 };
let users = [ john, pete, mary ];
let names = users.map(item => item.name);
alert( names ); // John, Pete, Mary
프로퍼티 age가 있는 객체가 담긴 배열이 있습니다. 이 배열을 age를 기준으로 정렬해주는 함수 sortByAge(users)를 만들어보세요.
예시:
let john = { name: "John", age: 25 };
let pete = { name: "Pete", age: 30 };
let mary = { name: "Mary", age: 28 };
let arr = [ pete, john, mary ];
sortByAge(arr);
// now: [john, mary, pete]
alert(arr[0].name); // John
alert(arr[1].name); // Mary
alert(arr[2].name); // Pete
function sortByAge(arr) {
arr.sort((a, b) => a.age - b.age);
}
let john = { name: "John", age: 25 };
let pete = { name: "Pete", age: 30 };
let mary = { name: "Mary", age: 28 };
let arr = [ pete, john, mary ];
sortByAge(arr);
// now sorted is: [john, mary, pete]
alert(arr[0].name); // John
alert(arr[1].name); // Mary
alert(arr[2].name); // Pete
문제를 풀 때 주의할 점은 모든 순열이 동일한 확률로 일어나야 한다는 점입니다. 예를 들어 [1,2,3] 은 [1,2,3]이나 [1,3,2], [3,1,2]로 재정렬 될 수 있는데, 이 배열들이 만들어지는 빈도는 같아야 합니다.
간단한 해결책은 아래와 같습니다.
function shuffle(array) {
array.sort(() => Math.random() - 0.5);
}
let arr = [1, 2, 3];
shuffle(arr);
alert(arr);
Math.random() - 0.5의 계산 결과는 양수나 음수 둘 중 하나이기 때문에 정렬 함수는 요소를 무작위로 재 정렬해줍니다.
그런데 sort는 이런 용도로 만들어진 메서드가 아니기 때문에 위와 같이 답안을 작성하면 숫자 1과 2, 3으로 만들 수 있는 순열이 같은 빈도로 나타나지 않습니다.
예시를 이용해 이를 살펴봅시다. 아래 코드는 함수 shuffle을 백만 번 실행시키고 가능한 한 모든 결과의 빈도를 세줍니다.
function shuffle(array) {
array.sort(() => Math.random() - 0.5);
}
// 1, 2, 3으로 만들 수 있는 모든 순열의 빈도를 세줍니다.
let count = {
'123': 0,
'132': 0,
'213': 0,
'231': 0,
'321': 0,
'312': 0
};
for (let i = 0; i < 1000000; i++) {
let array = [1, 2, 3];
shuffle(array);
count[array.join('')]++;
}
// 만들 수 있는 모든 순열의 생성 빈도를 세서 출력해줍니다.
for (let key in count) {
alert(`${key}: ${count[key]}`);
}
위 코드를 실행하면 아래와 유사한 결과가 도출됩니다(자바스크립트 엔진마다 다를 수 있습니다).
123과 213이 나타나는 빈도가 높은 것으로 보아 기존 코드를 이용하면 결과가 한쪽으로 쏠릴 수 있다는 것을 알 수 있습니다.
실행 결과는 자바스크립트 엔진마다 다르겠지만, 기존 코드는 문제에서 제시한 조건을 만족하지 못한다는 사실을 알게 되었습니다.
왜 위 코드는 의도한 대로 동작하지 않는 걸까요? 그 이유는 sort를 실행했을 때 내부 동작이 블랙박스 안에 담겨있기 때문입니다. sort를 실행하면 인수로 넘긴 정렬 함수가 배열을 정리해주는데 이 과정에서 배열 요소끼리의 비교가 완전 무작위로 이뤄지기 때문에 블랙박스 안에 무엇이 담겨있을지는 더 예측하기 어려워집니다. 자바스크립트 엔진마다 내부 구현방식이 다르므로 이런 혼돈은 더 커지죠.
이런 문제는 다양한 방법으로 해결할 수 있는데 피셔-예이츠 셔플(Fisher-Yates shuffle) 알고리즘은 이 중 하나입니다. 피셔-예이츠 셔플 알고리즘은 배열 끝 요소부터 시작해 앞으로 하나씩 나아가면서 해당 요소 앞에 있는 임의의 요소와 해당 요소를 바꿔치기하는 알고리즘입니다.
function shuffle(array) {
for (let i = array.length - 1; i > 0; i--) {
let j = Math.floor(Math.random() * (i + 1)); // 무작위 인덱스(0 이상 i 미만)
// array[i]와 array[j]를 바꿔치기합니다.
// 아래 답안에선 "구조 분해 할당(destructuring assignment)"이라 불리는 문법을 사용하여
// 원하는 것을 구현하였는데,
// 이 문법에 대한 자세한 내용은 이후 챕터에서 다룰 예정입니다.
// 구조 분해 할당을 사용하지 않고 작성한 코드는 아래와 같습니다.
// let t = array[i]; array[i] = array[j]; array[j] = t
[array[i], array[j]] = [array[j], array[i]];
}
}
자 이제 새롭게 작성한 함수를 가지고 실행해 봅시다.
function shuffle(array) {
for (let i = array.length - 1; i > 0; i--) {
let j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
}
// 1, 2, 3으로 만들 수 있는 모든 순열의 빈도를 세줍니다.
let count = {
'123': 0,
'132': 0,
'213': 0,
'231': 0,
'321': 0,
'312': 0
};
for (let i = 0; i < 1000000; i++) {
let array = [1, 2, 3];
shuffle(array);
count[array.join('')]++;
}
// 만들 수 있는 모든 순열의 생성 빈도를 세서 출력해줍니다.
for (let key in count) {
alert(`${key}: ${count[key]}`);
}
추가 코멘트, 질문 및 답변을 자유롭게 남겨주세요. 개선해야 할 것이 있다면 댓글 대신 이슈를 만들어주세요.
잘 이해되지 않는 부분은 구체적으로 언급해주세요.
댓글에 한 줄짜리 코드를 삽입하고 싶다면 <code> 태그를, 여러 줄로 구성된 코드를 삽입하고 싶다면 <pre> 태그를 이용하세요. 10줄 이상의 코드는 plnkr, JSBin, codepen 등의 샌드박스를 사용하세요.
You are visitor 01402503 — GeezerVison does not store the pages you look up. Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.