본문은 JavaScript 언어에 대한 이해를 높이기 위해 "You Don't Know JS(카일 심슨 저)"를 읽고 공부한 내용을 정리한 글입니다. 공부가 목적이기 때문에 필요에 따라 생략이나 수정, 추가된 부분이 있을 수 있습니다.
1. 배열
자바스크립트 배열은 다른 언어와 달리 객체 등의 어떤 타입의 값이라도 담을 수 있는 자료구조다. C나 자바와 달리 배열의 크기는 미리 정하지 않고도 생성할 수 있다.
const a = [];
a[0] = 1;
a[1] = "2";
a[2] = [3];
a.length; // 3
또한, 다음과 같은 빈 슬롯이 있는 구멍난(sparse) 배열을 만들 수도 있다. 아래처럼 5번 인덱스에 값을 담으면 0~4번 인덱스엔 빈 값이 담긴다. 하지만, 이 빈 값이 a[1] = undefined와 같이 명시적으로 값을 담은 것과 동일하지는 않다.
const a = [];
a[5] = 1;
a.length; // 6
a[1]; // undefined
a; // (6) [empty × 5, 1]
배열 인덱스는 숫자지만, 배열 자체도 하나의 객체이기 때문에 아래와 같이 키/프로퍼티로서 문자열을 추가할 수 있다. 이때, 배열의 길이는 증가하지 않는다.
const a = [];
a [0] = 1;
a["foo"] = 2;
a.length; // 1
a["foo"]; // 2
a.foo; // 2
여기서 하나의 문제가 발생하는데, 키로 넣은 문자열 값이 표준10진수 숫자로 타입이 바뀌면 인덱스를 사용한 것 같은 결과가 발생한다.
const a = [];
a["13"] = 42;
a.length // 14;
2. 문자열
자바스크립트 문자열은 다음과 같이? 배열과 유사하게 동작한다. length, indexOf(), concat() 등을 사용할 수 있으며, 인덱스를 사용해 문자열 내 특정 문자에 접근할 수 있다. 하지만, 문자열은 불변 값(Immutable)이지만 배열은 가변 값(Mutable)이라는 큰 차이가 있다. 따라서 문자열에서 사용할 수 있는 메소드들은 문자열 자체를 변경하는 것이 아니라 항상 새로운 문자열을 생성한 후 반환하는 것이다.
이에 따른 제약이 있기 때문에 만약 문자열 자체에 어떤 작업을 빈번하게 수행해야 한다면 문자가 담긴 배열을 이용하여 작업을 수행하는 것이 낫다. 이렇게 만든 문자 배열에 join()을 사용하여 간단하게 문자열로 변환할 수 있다.
// https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Array/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"
3. 숫자
자바스크립트의 숫자 타입은 number가 유일하며, 이는 정수(Integer)와 부동 소수점 숫자(Fractional Decimal Number)를 모두 아우른다.
큰 숫자는 보통 다음과 같이 지수형으로 표시한다.
const a = 1E3; // 1 * 10^3
const b = 1.1E6; // 1.1 * 10^6
숫자 리터럴은 다른 언어와 마찬가지로 10진법 뿐만 아니라 2진법, 8진법, 16진법을 이용해 나타낼 수 있다.
0363; // 243의 8진수
0xf3; // 243의 16진수
0Xf3; // 위와 같음
/**
* ES6부터는 아래와 같이 쓸 수 있다
*/
0b11110011; // 243의 이진수
0B11110011; // 위와 같음
0o363; // 243의 8진수
0O363; // 위와 같음
작은 소수 값
다음은 널리 알려진 이진 부동소수점 숫자의 부작용 문제다.
0.1 + 0.2 === 0.3; // false
0.1 + 0.2; // 0.30000000000000004
이는 자바스크립트가 IEEE 754 표준에 따라 숫자를 부동 소수점 방식으로 처리하기 때문에 발생한 문제이다.
쉽게 말하면 자바스크립트에서 0.1과 0.2 같은 수는 10진수를 2진수로 변환하는 과정에서 무한 소수가 발생하기 때문에 정확한 숫자가 아닌 매우 근접한 숫자인 것이다. 이에 대한 자세한 설명은 여기서 확인할 수 있다.
어떻게 두 숫자를 비교할까?
1. 자바스크립트용 수학 라이브러리를 사용한다.
다음은 math.js를 이용한 연산이다.
math.add(math.fraction(0.1), math.fraction(0.2)); // Fraction, 0.3
math.divide(math.fraction(0.3), math.fraction(0.2)); // Fraction, 1.5
2. toFixed()를 이용한다.
이때, toFixed()는 문자열을 반환한다는 점을 주의해야 한다.
(0.1 * 0.2).toFixed(2); // "0.02"
3. 미세한 '반올림 오차'를 허용 공차(Tolerance)로 처리한다.
이렇게 미세한 오차를 머신 입실론(Machine Epsilon)이라 하며, 자바스크립트 숫자의 머신 입실론은 2^-52다.
ES6부터는 이 값이 Number.EPSILON으로 정의되어 있으므로 다음과 같은 방법으로 두 숫자의 동등함을 비교할 수 있다.
function isEqual(num1, num2) {
return Math.abs(num1 - num2) < Number.EPSILON
}
const a = 0.1 + 0.2;
const b = 0.3;
isEqual(a, b); // true
isEqual(0.0000001, 0.0000002); // false
부동 소수점의 최댓값은 Number.MAX_VALUE(대략 1.798e+308)로 정의되어 있으며, 최솟값은 Number.MIN_VALUE(대략 5e-324)
안전한 정수 범위
표현한 값과 실제 값이 정확하게 일치한다고 장담할 수 있는 것을 안전(Safe)하다고 한다. 자바스크립트에서 안전하게 표현할 수 있는 정수는 최대 2^53 - 1이며, ES6에서 Number.MAX_SAFE_INTEGER로 정의되어 있다. 반대로 최솟값은 -(2^53 - 1)이며, Number.MIN_SAFE_INTEGER로 정의되어 있다.
4. 특수 값
타입별로 자바스크립트 개발자들이 조심해서 사용해야 할 특수할 값들이 있다.
값 아닌 값
- undefined 타입의 값은 undefined밖에 없으며, null 타입의 값 또한 null밖에 없다. 따라서 이 둘은 타입과 값이 항상 일치한다.
- void 연산자는 어떤 값이든 항상 결과값을 undefined로 만든다.
const x = 1;
void x; // undefined
x; // 1
NaN
수학 연산 시 두 피연산자 중 하나라도 숫자가 아니라면 그 결과는 NaN이다. NaN은 숫자가 아님(Not A Number)을 의미하는데, 경계 값의 일종으로 숫자 집합 내에서 특별한 종류의 에러 상황을 나타낸다.
다음을 보자. 숫자 아님의 타입이 숫자라는 이상한 상황이 발생한다.
const a = 2 / "foo"; // NaN
typeof a === "number"; // true
따라서 유효하지 않은 숫자(Invalid Number)나 실패한 숫자(Failed Number)로 해석하는 편이 더 정확하다.
또한, NaN여부는 ES6 이후부터 등장한 Number.isNaN()을 이용해 정확하게 확인할 수 있다.
const a = 2 / "foo";
const b = "foo";
Number.isNaN(a); // true
Number.isNaN(b); // false
isNaN()의 경우 인자 값이 숫자인지 여부만을 평가하기 때문에 정확하게 NaN을 확인할 수 없다.
const a = 2 / "foo";
const b = "foo";
Number.isNaN(a); // true
Number.isNaN(b); // true
무한대
양의 무한대는 Number.POSTIVE_INFINITY, 음의 무한대는 Number.NEGATIVE_INFINTY로 표현할 수 있다.
5.값 vs 레퍼런스
다른 언어에서 값은 사용하는 구문에 따라 값-복사(Value-Copy) 또는 레퍼런스-복사(Reference-Copy)의 형태로 할당/전달한다. 자바스크립트는 C와 달리 포인터라는 개념이 존재하지 않고, 어떤 변수가 다른 변수를 참조할 수 없다.
자바스크립트에서 레퍼런스는 공유된 값을 가리킨다. 따라서 서로 다른 복수의 레퍼런스가 존재한다면 이들은 저마다 항상 공유된 단일 값을 개별적으로 참조한다. 이 말이 다소 어려운데, 그림으로 표현하자면 다음과 같다.
또한, 자바스크립트에는 값 또는 레퍼런스의 할당/전달을 제어하는 구문 암시가 없다. 값의 타입만으로 값-복사, 레퍼런스-복사 둘 중 한쪽이 결정된다.
- null, undefined, string, number, boolean, symbol 같은 단순 값(스칼라 원시 값 - Scalar Primitives)은 언제나 값-복사 방식으로 할당/전달한다.
- 객체나 함수 등 합성 값(Compound Values)은 할당/전달시 반드시 레퍼런스 사본을 생성한다.
다음의 예제를 살펴보자.
const a = 2;
const b = a; // b는 언제나 a에서 값을 복사한다.
b++;
a; // 2
b; // 3
const c = [1, 2, 3];
const d = c; // d는 공유된 값([1,2,3])의 레퍼런스다.
d.push(4);
c; // [1, 2, 3, 4]
d; // [1, 2, 3, 4]
- 2는 스칼라 원시 값이므로 a엔 이 값의 초기 사본이 들어가고, b에는 또 다른 사본이 들어간다.
- 반면, c와 d는 합성 값이면서 동일한 공유 값 [1, 2, 3]에 대한 개별 레퍼런스다.
여기서 중요한 점은 c와 d가 [1, 2, 3]을 '소유'하는 것이 아니라 단지 이 값을 동등하게 '참조'만 한다는 사실이다. 따라서 위와 같이 push()를 통해 레퍼런스로 실제 공유한 배열의 값이 변경되면, 두 레퍼런스는 갱신된 값 [1, 2, 3, 4]를 동시에 바라보게 된다.
레퍼런스는 변수가 아닌 값 자체를 가리키므로 A레퍼런스로 B레퍼런스가 가리키는 대상을 바꿀 수는 없다. 앞서 말했듯이 자바스크립트엔 포인터라는 개념이 없기 때문이다.
const a = [1, 2, 3];
const b = a;
a; // [1, 2, 3]
b; // [1, 2, 3]
b = [4, 5, 6];
a; // [1, 2, 3]
b; // [4, 5, 6]
함수 인자도 유사하게 작동한다. 아래 코드에서 a를 인자로 넘기면 a의 레퍼런스 사본이 x에 할당된다. x와 a는 모두 동일한 [1, 2, 3] 값을 가리키는 별도의 레퍼런스다.
function foo(x) {
x.push(4);
x; // [1, 2, 3, 4]
x = [4, 5, 6];
x.push(7);
x; // [4, 5, 6, 7]
}
const a = [1, 2, 3];
foo(a);
a; // [4, 5, 6, 7]이 아닌 [1, 2, 3, 4]
비교연산
// 원시 값
const one = 1;
const oneCopy = 1;
console.log(one === oneCopy); // true
console.log(one === 1); // true
console.log(one === one); // true
// 합성 값
const ar1 = [1];
const ar2 = [1];
console.log(ar1 === ar2); // false
console.log(ar1 === [1]); // false
const ar11 = ar1;
console.log(ar1 === ar11); // true
console.log(ar1 === ar1); // true
- 원시 값의 경우 변수의 값, 리터럴과 상관 없이 값(Value)이 동일한지 여부에 따라 판별한다.
- 합성 값의 경우 값이 동일하더라도 같은 객체를 참조하고 있는지 여부에 따라 판별한다.
6. 참조
'프로그래밍 언어 > JavaScript + TypeScript' 카테고리의 다른 글
[You Don't Know JS] 3. 네이티브(Native) (0) | 2021.06.24 |
---|---|
[스크랩] JavaScript(ES6), TypeScript, Node.js 개발자를 위한 무료 e북 4가지 (0) | 2021.06.22 |
[You Don't Know JS] 1. 타입(Type) (0) | 2021.06.21 |
[JavaScript] 함수 선언문 vs 함수 표현식 (0) | 2021.03.23 |
[JavaScript] 펼침 연산자: spread operator (0) | 2021.02.17 |