You Don't Know JS #3

YoungToMaturity·2021년 7월 7일
0

YDKJS🧔

목록 보기
4/4
post-thumbnail

숫자

자바스크립트의 숫자 타입은 Number가 유일하며, 정수(Integer)와 부동 소수점 숫자를 모두 아우른다.

자바스크립트를 포함한 대부분의 현대 프로그래밍 언어는 'IEEE 754' 표준을 따르며, 10진수 리터럴로 표시한다.

아주 크거나 아주 작은 숫자는 지수형으로 표시하며,toExponential() 메서드의 결과값과 같다.

var a = 5E10;
a; // 50000000000
a.toExponential(); // "5e+10"

var b = a * a;
b; // 2.5e+21

var c = 1 / a;
c; // 2e-11

숫자 구문

숫자 값은 Number 객체 Wrapper로 Boxing 할 수 있기 때문에 Number.prototype 메서드로 접근 할 수 있다. 숫자 리터럴에 변수를 만들어서 할당하지 않아도 되지만, .이 소수점인 경우엔 프로퍼티 접근자가 아닌 숫자 리터럴의 일부로 해석되므로, .연산자를 사용할 때는 조심하자.

// 잘못된 구문
42.toFixed(3); // Syntax Error

// 모두 올바른 구문
(42).toFixed(3); // "42.000"
0.42.toFixed(3); // "0.420"
42..toFixed(3); // "42.000"

작은 소수 값

앞서 언급한 IEEE 754 표준을 따르는 모든 언어에서 공통적으로 발생하는 문제가 있다.

0.1 + 0.2 === 0.3; // false

수학의 영역에서 생각해본다면 당연히 true이지만, 실제 결과는 false이다.
그 이유는 이진 부동 소수점으로 나타낸 0.10.2는 원래의 숫자와 일치하지 않는다. 그렇기 때문에 값은 0.3이 아닌 0.30000000000000004에 가깝다. 그렇기 때문에 false를 반환하는 것이다.

true를 반환받고 싶다면 어떻게 해야할까?
가장 일반적인 방법은 이 작은 '반올림 오차'를 허용 공차(Tolerance)로 처리하는 방법이다. 이 미세한 오차를 머신 입실론이라고 하는데, javascript의 머신 입실론은 2^-52이다.
ES6부터는 이 값을 Number.EPSILON을 통해 사용할 수 있다.

안전한 정수 범위

javascript로 표현 할 수 있는 가장 큰 수와 작은 수인 Number.MAX_VALUENumber.MIN_VALUE가 존재하지만, 머신 입실론의 존재 때문에 표현한 값과 실제 값이 정확하게 일치한다고 장담할 수 있는 정수의 범위는 그보다 작다.
이를 안전하게 표현할 수 있는 정수 범위라고 하고, ES6에서 Number.MAX_SAFE_INTEGERNumber.MIN_SAFE_INTEGER로 정의해 두었다.

javascript에서 안전한 정수 범위를 넘어서는 크기의 숫자를 마주하는 가장 빈번한 경우는 데이터 베이스 등에서 64bit ID를 처리할 때이다. 그렇기 때문에 javascript에서 이 64bit 숫자 타입을 주고 받을 때에는 string 타입으로 저장을 해야한다.
만약 연산을 해야하는 상황이 발생한다면, Big Number 유틸리티를 사용하는 것을 추천한다. (BigInteger.js Github repository 참고)

정수인지 확인

javascript는 여타 일반적인 프로그래밍 언어들과 달리, Integer 타입을 따로 지정하지 않는다. 그렇기 때문에 Number 타입의 값이 정수인지 여부를 확인해야할 경우가 종종 발생한다.
ES6부터는 Number.isInteger()로 어떤 값의 정수 여부를 확인할 수 있다.

Number.isInteger(42); // true
Number.isInteger(42.000); // true
Number.isInteger(42.3); // false

Number.isSafeInteger()을 통해 안전한 정수 여부도 확인할 수 있다.

32비트 (부호있는) 정수

정수의 '안전 범위'가 대략 9천조에 이르지만, 32비트 숫자에만 가능한 연산이 있으므로 실제 범위는 훨씬 줄어든다.
따라서 정수의 안전 범위는 Math.pow(-2,31)에서 Math.pow(2,31) - 1까지다.
a | 0과 같이 쓰면 '숫자 값 -> 32비트 (부호있는) 정수'로 강제 변환을 한다. | 비트 연산자는 32비트 정수 값에만 쓸 수 있기 때문에 가능한 방법이다.
0과 OR연산은 본질적으로 NOOP 비트 연산과 같다

NOP 또는 NOOP은 어셈블리 언어의 명령어 중 하나로, 명령 자체의 길이만큼 프로그램 카운터를 증가시킬 뿐 아무런 실행도 하지 않습니다. 0과 OR의 연산 역시 값은 변하지 않으므로 NOOP 비트 연산과 같다는 비유를 했습니다

특수 값

값 아닌 값

undefined 타입의 값은 undefined 밖에 없고, null 타입의 값 또한 null 뿐이다. undefinednull은 주로 빈(Empty)값과 아닌(Nonvalue)값을 나타낸다. 다른 의미로 사용되는 예시는

  • null빈 값이다.
  • undefined실종된(Missing) 값이다.
  • null은 예전에 값이 있었지만 지금은 없는 상태다.
  • undefined는 값을 아직 가지지 않은 것이다.

Undefined

느슨한 모드에서는 전역 스코프에서 undefined라는 식별자에 값을 할당할 수 있다. (null이라는 변수는 할당할 수 없다. 두 방법 모두 절대 추천하지 않는 방법이다!)

function foo() {
  undefined = 2; // 정말 추천하지 않는다!
}
foo();

function foo() {
  "use strict";
  undefined = 2; // 타입 에러 발생!
}
foo();

심지어 지역 변수의 경우, 모드에 상관없이 undefined를 생성할 수 있다. 역시나 추천하지 않는 방법이며 끔찍하다!

function foo() {
  "use strict";
  var undefined = 2;
  console.log(undefined); // 2
}
foo();

void 연산자
undefined는 내장 식별자로, 값은 undefined이지만, 이 값은 void 연산자로도 얻을 수 있다.
표현식 void __는 어떤 값이든 무효로 만들어, 항상 결과값을 undefined로 만든다. 기존의 값은 건드리지 않고, 연산 후 값은 복구할 수 없다.
var a = 42;
console.log(void a, a); // undefined 42
관례에 따라, undefined의 값을 void만으로 나타내려면, void 0이라고 쓴다. void 0, void 1, undefined 모두 같다.
void 연산자는 값이 존재하는 곳에서 그 값이 undefined가 되어야 좋을 경우에만 사용하도록 하자. 아마도 그렇게 해야할 경우도 거의 없고, 극히 제한적으로 쓰이겠지만 제법 쓸모는 있다.

특수 숫자

The not number, number
수학 연산 시 두 피연산자 중 하나라도 숫자가 아닐 경우 유효한 숫자가 나올 수 없으므로 결과는 NaN이다.
NaN은 글자 그대로 Not a Number이지만 숫자가 아니다 보다는 유효하지 않은 숫자, 실패한 숫자, 몹쓸 숫자라고 하는게 차라리 더 정확하다.
var a = 2 / "foo"; // NaN
typeof a === "number"; // true
그 이유는 NaNtypeofNumber이기 때문이다!
즉, NaN은 경계 값의 일종으로, 숫자 집합 내에서 특별한 종류의 에러 상황을 나타낸다.
그렇다면 특정 상황의 어떤 변수값이 NaN인지 확인할 때, nullundefined처럼 직접 비교를 할 수 있을까?
a == NaN; // false
a === NaN; // false
NaN은 다른 어떤 NaN 값과도 동등하지 않다. 심지어 자기 자신과도 같지 않으며 이를 반사성이 없다고 한다.x === x; // false
javascript 상의 반사성이 없는 값은 NaN이 유일하다
그렇다면 NaN 여부를 확인하는 방법에는 어떤 것이 있을까?
isNaN(a); // true, 간단하다!
하지만, isNaN()NaN유효하지 않은 숫자,실패한 숫자뿐만 아니라, 숫자가 아닌 모든 경우 true를 반환한다.
isNaN("foo"); // true
하지만 이 버그는 ES6에서 Number.isNaN() 함수가 등장하면서 수정되었다.
해당 함수를 사용하지 않고도 간단하게 NaN을 확인할 수 있다. 반사성이 없다는 특징을 활용한 방식이다.
if(n !== n) return n;

무한대
C를 경험한 적 있는 개발자들은 Divide By Zero와 같은 에러를 자주 겪었을 것이다.
하지만 javascript는 0으로 나누는 경우 에러없이 Infinity(Number.POSITIVE_INFINITY)라는 결과값이 나온다.
분자가 음수인 경우는 -Infinity(Number.NEGATIVE_INFINITY라는 결과값이 나온다.
let a = 1 / 0; // Infinity
let b = -1 / 0; // -Infinity
IEEE 754 명세에 따르면, 연산 결과가 너무 커서 표현하기 곤란할 때, 가장 가까운 수로 반올림(Round-To-Nearest)모드가 결과값을 정한다.
무한을 무한으로 나누면 -> 정의되지 않은 연산, NaN이다.
유한한 양수를 무한대로 나누면 -> 0
유한한 음수를 무한대로 나눈다면?

영(0)
javascript의 0에는 +0-0이 존재한다. 표기만 -0으로 하는 것이 아니라, 특정 수식의 연산 결과 또한 -0으로 떨어진다.
let a = 0 / -3; // -0
let b = 0 * -3; // -0
하지만 -0을 문자열화(stringify)하면 항상 "0"이다.
신기한 점은 "-0"을 숫자화(parse)하면 -0이 된다.
또한 -0의 비교도 어렵다.
-0 == 0; // true
-0 === 0; // true
0 > -0; // false
이러한 -0은 javascript에 왜 생기게 되었을까?
값의 크기로 어떤 정보(ex: 애니메이션 프레임당 넘김 속도)와 그 값의 부호로 또 다른 정보(ex: 넘김 방향)를 동시에 나타내야 하는 어플리케이션이 있기 때문이다.
+0, -0 개념이 없다면, 어떤 변수값이 0에 도달하여 부호가 바뀌는 순간까지 변수의 이동 방향을 알 수가 없기 때문에 유용하다.

특이한 동등 비교

NaN-0의 동등 비교는 독특하다. 이러한 불편함을 줄이기 위해 ES6에서는 Object.is()를 활용하여 두 값이 절대적으로 동등한지를 확인하는 유틸리티를 제공한다.
=====가 안전하다면 Object.is()에 비해 더 효율이 좋고 일반적이기 때문에 Object.is()를 사용하지 않는 편이 좋지만, 지금까지 언급된 특이한 동등 비교에는 Object.is()를 사용한다.

값 vs 레퍼런스

다른 언어에서 값은 값-복사(Value-Copy) 또는 레퍼런스-복사(Reference-Copy)의 형태로 할당/전달한다.
javascript에서는 포인터라는 개념 자체가 없고 참조하는 방법도 조금 다르다. 예시로 어떤 변수가 다른 변수를 참조할 수 없다.
javascript에서 레퍼런스는 (공유된) 값을 가리키므로 서로 다른 10개의 레퍼런스가 있다면 이들은 저마다 항상 공유된 단일 값(서로에 대한 레퍼런스/포인터 따위는 없다)을 개별적으로 참고한다.
javascript는 값의 타입만으로 값-복사, 레퍼런스-복사 둘 중 한쪽이 결정된다.
null,undefined,string,number,boolean 그리고 ES6의 symbol 같은 단순 값(스칼라 원시 값)은 언제나 값-복사 방식으로 할당/전달된다.
객체나 함수 등 합성 값은 할당/전달 시 반드시 레퍼런스 사본을 생성한다.

let a = 2;
let b = a; // 'b'는 언제나 'a'에서 값을 복사한다.
b++;
a; // 2
b; // 3

let c = [1,2,3];
let d = c // 'd'는 공유된 '[1,2,3]' 값의 레퍼런스다.
d.push(4);
c; // [1,2,3,4]
d; // [1,2,3,4]

2는 스칼라 원시 값이기 때문에 a에는 이 값의 초기 사본이 들어가고, b에는 또 다른 사본이 자리를 잡는다. 따라서 b를 바꿈으로써 a까지 동시에 값을 변경할 방법은 없다.
하지만, cd는 모두 합성 값이자 동일한 공유 값 [1,2,3]에 대한 개별 레퍼런스다. 여기서 기억해야 할 점은 cd[1,2,3]소유하는 것이 아니라 단지 이 값을 동등하게 참조만 한다는 사실이다.
따라서 레퍼런스로 실제 공유한 배열 값이 변경되면 .push(4), 이 공유 값 한 군데에만 영향을 미치므로 두 레퍼런스는 갱신된 값 [1,2,3,4]를 동시에 바라보게 된다.
중요한 점은, 레퍼런스는 변수가 아닌 값 자체를 가리키므로, 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 = [4,5,6]으로 할당해도 a가 참조하는 [1,2,3]은 영향을 받지 않는다. 그렇게 되려면 b가 배열을 가리키는 레퍼런스가 아닌 포인터가 되어야 하는데, javascript에는 포인터가 없다!

function foo(x) {
  x.push(4);
  x; // [1,2,3,4]
  
  x = [4,5,6];
  x.push(7);
  x; // [4,5,6,7]
}

let a = [1,2,3];

foo(a);

a; // [4,5,6,7]이 아닌 [1,2,3,4]

a를 인자로 넘기면 a의 레퍼런스 사본이 x에 할당된다. 이제 함수 내부에서 이 레퍼런스를 이용하여 값 자체를 변경한다(.push(4)). 하지만 그 후 x = [4,5,6]으로 새 값을 할당해도 초기 레퍼런스 a가 참조하고 있던 값에는 아무런 영향이 없다. 즉 a 레퍼런스는 여전히 [1,2,3,4] 값을 바라보고 있다.
레퍼런스 xa가 가리키고 있는 값을 바꿀 도리는 없다. 단지 ax 둘 다 가리키는 공유 값의 내용만 바꿀 수 있다.

배열을 새로 생성하여 할당하는 방식으로는 a의 내용을 [4,5,6,7]로 바꿀 수 없다. 기존에 존재하는 배열 값만 변경해야한다.
값-복사 or 레퍼런스-복사를 내가 결정할 수 없음을 기억하자. 전적으로 값의 타입을 보고 엔진의 재량으로 결정된다.
(배열과 같은) 합성 값을 값-복사에 의해 효과적으로 전달하려면 손수 값의 사본을 만들어 전달한 레퍼런스가 원본을 가리키지 않게 하면 된다.
foo(a.slice());

정리하기

javascript 배열은 모든 타입의 값들을 숫자로 인덱싱한 집합이다.
문자열은 일종의 '유사배열'이지만, 나름의 특성이 있기 때문에 배열로 다루고자 할때는 주의해야 한다.
javascript Number는 '정수'와 '부동 소숫점 숫자' 모두를 포함한다.

원시 타입의 특수한 값

  • null 타입은 null 값만, undefined 타입은 undefined 값 하나만 갖는다.
  • undefined는 할당된 값이 없다면 모든 변수/프로퍼티의 default 값이다.
  • void 연산자는 어떤 값이라도 undefined로 만들어버린다.

Number에는 NaN, +Infinity,-Infinity,-0 과 같은 특수 값이 있다.
단순 스칼라 원시 값은 값-복사에 의해, 합성 값(객체 등)은 레퍼런스-복사에 의해 값이 할당/전달된다.
javascript에서의 레퍼런스는 다른 언어의 레퍼런스/포인터와는 전혀 다른 개념으로, 또 다른 변수/레퍼런스가 아닌 오직 자신의 값만을 가리킨다.

profile
iOS Developer

0개의 댓글