자바스크립트 (이하 JS) 배열은 타입이 엄격한 다른 언어와 달리 어떤 타입의 값도 담을 수 있는 상자입니다.
let arr = [1, '2', [3]]; // no error
여기서 주의할 점은 슬롯을 건너뛸 수 있다는 점입니다.
let arr = [];
arr[0] = 0;
arr[2] = 2;
arr[1]; // undefined
arr.length; // 3
1번째 인덱스에 값을 넣지 않고 2번째 인덱스에 값을 넣는 경우에는 a[1]은 undefined면서 arr 배열의 길이는 3이 됩니다.
여기서 arr의 인덱스가 건너뛰더라도 값을 삽입한 마지막 인덱스에 따라 배열의 길이가 설정된다는 점을 알 수 있습니다.
배열 자체는 하나의 객체이기 때문에 배열 인덱스는 숫자이지만 key - value 형식으로 문자열을 추가할 수도 있습니다. 이 때에는 배열 length가 증가하지 않는다는 점을 유의하여야 합니다.
배열을 사용할 때에는 문자열 타입의 key - value를 웬만하면 사용하지 않고 숫자를 인덱스로 확실히 사용하는 것이 좋은 습관입니다.
흔히 문자열은 문자의 배열이라고 생각하기 쉽습니다. JS에서 문자열은 문자 배열과 다릅니다.
그러므로, 문자열은 항상 내용을 바로 변경하지 않고 새로운 문자열을 생성한 후 반환합니다.
반면, 배열은 그 자리에서 곧바로 원소를 수정합니다.
부가적으로, 다음은 문자열의 순서를 바꾸는 코드입니다.
let reversedStr = str.split('').reverse().join('');
이런 식으로 배열의 reverse 메서드를 사용하기 위해 문자열을 배열로 변환했다가 순서를 바꾼 후 다시 문자열로 만드는 방법이 있습니다.
JS에서 모든 숫자는 number 타입 하나로만 표시됩니다.
const a = 42; // 정수
const b = 42.3; // 실수
const c = .42; // 소수점 앞 정수가 0이면 생략 가능
const d = 5E10; // 50000000000같이 아주 크거나 작은 숫자는 지수형으로 표시
const e = 42.; // 42
JS에서 정수는 부동 소수점이 없는 값입니다.
let a = 12.35;
a.toFixed(0); // "12"
a.toFixed(1); // "12.4"
a.toFixed(2); // "12.35"
a.toFixed(3); // "12.350"
a.toFixed(4); // "12.3500"
toFixed 메서드는 지정한 소수점 이하 자릿수까지 숫자를 나타냅니다. (반올림 처리)
let b = 34.56;
b.toPrecision(1); // "3e+1"
b.toPrecision(2); // "34"
b.toPrecision(3); // "34.5"
b.toPrecision(4); // "34.56"
b.toPrecision(5); // "34.560"
b.toPrecision(6); // "34.5600"
toPrecision 메서드도 toFixed와 비슷하지만 유효 숫자 개수를 지정할 수 있습니다.
여기서 주의해야 할 점은
42.toFixed(3); // SyntaxError <-- 42. = 42이므로
(42).toFixed(3); // working!
42.가 42로 취급된다는 사실을 유의하여야 합니다.
const a = 0o363; // 243의 8진수
const b = 0b1110011; // 243의 2진수
또한, 다음과 같이 2진수, 8진수, 16진수 등을 표현할 수 있습니다.
0 다음에 소문자로 o, b, x를 적는 것이 가독성에 도움이 되므로 소문자를 적는 습관이 가지는 것이 좋습니다.
많은 언어들에게서 나타나는 문제를 다뤄보겠습니다.
0.1 + 0.2 === 0.3; // false
부동 소수점 숫자들을 비교할 때에 흔히 나타나는 오류입니다. 위의 예가 false인 이유는 실제로는 0.3이 아니라 0.30000000000000004 정도에 가깝다는 것이 이유입니다.
이러한 오류를 해결하기 위해서 우리는 Number.EPSILON으로 미세한 허용 공차 이내에 있는지 판단하여 오차 내에 있다면 같다고 처리하는 방법이 가장 일반적입니다.
function isSame(num1, num2) {
return Math.(num1 - num2) < Number.EPSILON;
}
const a = 0.1 + 0.2;
const b = 0.3;
isSame(a, b); // true
이렇기 때문에 JS에서는 실제로 표현할 수 있는 숫자와 안전 값의 범위가 따로 정해져 있습니다.
Number.MAX_VALUE와 Number.MIN_VALUE는 표현할 수 있는 최대, 최소 숫자이지만 이것이 실제 값과 정확하게 일치한다고 장담할 수 없습니다.
그래서, Number.MIN_SAFE_INTEGER와 Number.MAX_SAFE_INTEGER가 존재하며 이것은 표현 값과 실제 값이 정확하게 일치하는 최소, 최대 범위 기준이 됩니다.
Number.isInteger와 Number.isSafeInteger로 어떤 값의 정수 여부와 안전한 정수 여부를 파악할 수도 있습니다.
Number.isInteger(24); // true
Number.isInteger(24.0000); // true
Number.isInteger(24.3); // false
Number.isSafeInteger(Number.MAX_SAFE_INTEGER); // true
Number.isSafeInteger(Number.MAX_VALUE); // false
느슨한 모드에서는 undefined에 값을 할당할 수 있습니다. (절대 비추)
void + 어떤 값은 어느 값이든 무효로 만들어 항상 결과값을 undefined로 만듭니다.
let num = 24;
console.log(void num, num); // undefined 24
void 연산자는 어떤 표현식의 결과값이 없다는 것을 명확하게 밝혀야 할 때 요긴하게 사용됩니다.
if (...) {
return void doSomething();
}
if (...) {
doSomething();
return;
}
위의 2개의 조건문은 같은 기능을 수행합니다. 많은 개발자들은 2번째 방식처럼 코드를 두 줄로 분리해 쓰는 것을 선호합니다.
NaN은 '숫자 아님'이라고 정의하고 싶지만 정확히는 '유효하지 않은 숫자'입니다.
신기하게도, typeof NaN은 number입니다.
NaN은 유별나게도 다른 어떤 NaN과도 동등하지 않습니다. 즉, 자기 자신과도 같지 않습니다.
x === x가 아닌 유일무이한 값입니다.
그래서 우리는 NaN을 구별할 때 Number.isNaN을 사용합니다.
양수를 무한대로 나누면 우리는 쉽게 결과값이 0이라는 것을 계산할 수 있습니다.
그렇다면, 음수를 무한대로 나누면 결과가 어떨까요? 정답은 -0입니다.
let a = 0 / -1; // -0
let b = 0 * -1; // -0
a.toString(); // 0
String(a); // 0
JSON.stringify(a); // 0
Number("-0"); // -0
JSON.parse("-0"); // -0
특이한 점은 -0을 문자열로 변환하면 "0"이 되지만 "-0"을 숫자로 변환하면 -0이 유지된다는 점입니다.
-0 === 0; // true
-0 > 0; // false
0 > -0; // false
또한, 0과 -0는 구분되지만 동등한 값으로 취급됩니다.
하지만 이럼에도 불구하고 -0이 존재하는 이유는 값의 크기로 어떤 정보 (가령, 프레임 속도)와 그 값의 부호로 동시에 2개의 정보를 나타내야 하는 경우가 존재하기 때문입니다. 그리고, 0과 -0을 통해 변수의 이동 방향이 어떻게 되는지 알 수 있다는 점에서도 부호가 다른 2개의 0은 유용하게 쓰일 수 있습니다.
NaN과 -0같이 특별한 값들마저도 동등하게 비교할 수 있는 메서드는 Object.is()입니다.
let a = 2 / "hi";
let b = -100 * 0;
Object.is(a, NaN); // true
Object.is(b, -0); // true
Object.is(b, 0); // false
보통의 경우에 우리는 ===을 사용하는 것이 더 효율적이고 가독성이 좋습니다. Object.is()는 주로 특이한 동등 비교의 경우에 사용됩니다.
null, undefined, string, number, boolean, symbol은 항상 값-복사 방식으로 할당/전달됩니다.
객체나 함수 등의 compound values는 반드시 값을 할당/전달할 때, 레퍼런스 사본을 생성합니다.
레퍼런스는 변수가 아닌 값 자체를 가리키므로 A 레퍼런스에서 B 레퍼런스로 가리키는 대상을 변경할 수는 없습니다.
let a = [1, 2, 3];
let b = a;
a; // [1, 2, 3]
b; // [1, 2, 3]
b = [4, 5, 6];
a; // [1, 2, 3]
b; // [4, 5, 6]
b에 다른 배열이 할당되어도 a가 참조하는 배열에는 영향을 받지 않습니다. 그렇게 되려면 b가 배열을 가리키는 것이 레퍼런스가 아닌 포인터가 되어야 하는데, JS에는 포인터가 존재하지 않습니다.
function doSomething(arr) {
arr.push(4);
arr; // [1,2,3,4]
arr = [7,8,9];
arr.push(10);
arr; // [7,8,9,10]
}
let a = [1,2,3];
doSomething(a);
a; // [1,2,3,4]
doSomething 함수에서 a 배열이 파라미터로 들어가서 해당 배열 내에서 값이 추가되거나 변경되는 경우에는 그것이 반영되지만 객체 자체를 바꾸는 경우에는 더 이상 반영이 되지 않습니다. 이것이 JS에서 레퍼런스 사본을 통해 값을 할당하는 방식에서 중요한 특징입니다.
여기서 중요한 점은 값-복사냐 레퍼런스-복사냐는 우리가 결정할 수 없고 전적으로 값의 타입을 보고 JS 엔진이 결정합니다.