본문 바로가기

Language/JavaScript

<JS> 특수 값과 레퍼런스

1. 값 아닌 값

* undefined와 null

 -  자바스크립트에서는 undefined 타입과 null 타입이 존재한다. 이들은 각각 undefined와 null 값만을 갖는 타입이다. 즉 둘은 타입과 값이 같다. 둘 다 '빈 값' 또는 값이 아님을 의미한다. 하지만 둘은 엄연히 차이가 있다.

 

 - null은 식별자가 아닌 특별한 키워드이므로 변수를 할당할 수 없다. 그러나 undefined는 식별자로 쓸 수 있어서 변수를 할당할 수 있다.

 

 - 느슨한 모드에서는 전역 스코프에서도 undefined라는 식별자에 값을 할당할 수 있고, 심지어 모드에 상관없이 지역 변수를 생성할 수 있다.

 

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

 

 - 당연히 위와 같이 undefined 에 값을 할당하는 것은 매우 지양되는 패턴이다. 

 

 - undefined 는 void 연산자로도 얻을 수 있다. 표현식 void _ 는 어떤 값이든 undefined로 만든다. (관례에 따라 void만으로 undefined 값을 나타내려면 void 0 이라고 쓴다. void 0, void 1, undefined 모두 같다)

 

2. 특수 숫자

* NaN

- 수학 연산 시 두 피연산자가 전부 숫자가 아닐 경우 유효한 숫자가 나올 수 없으므로 그 결과는 NaN (Not A Number)이다. 그러나 이 명칭은 오해의 소지가 다분하다. 실제로 NaN은 '숫자가 아니다'라고 표현하기 보다 '유효하지 않은 숫자', '연산에 실패한 숫자' 등의 표현이 정확하다. 아래에서 볼 수 있듯 '숫자다 아니다'의 타입은 숫자이기 때문이다.

 

const x = 10 / "foo"; // NaN
typeof x === 'number'; // true

 

 - NaN 의 문제는 여기서 끝이 아니다. 어떤 변수가 NaN인지를 확인하기 위해 비교연산자를 사용하면 어떻게될까.

 

const x = 10 / "foo" // NaN
x == NaN; // false
x === NaN; // false
NaN !== NaN; // true

 

 - NaN은 반사성이 없는 (x === x 로 식별되지 않는) 유일무이한 값이다. 그렇다면 어떻게 비교해야할까. 이를 위해 isNaN이 존재한다.

 

const x = 10 / "foo";
isNaN(x); // true

 

 - 하지만 이 또한 문제가 있다. 이 함수는 특이하게 '숫자가 아니다'를 글자 그대로 해석한다. 즉 인자가 숫자인지 여부를 평가한다. 

 

const x = "foo";
isNaN(x); // true

 

 - "foo"는 숫자가 아니지만 NaN 도 아니다. 하지만 결과는 true이다. 이 버그는 JS가 탄생된 이후 수정되지 못했다.

 

 - 이를 해결하기 위해 ES6 부터 Number.isNaN( )이 등장했다. 이를 통해 안전하게 NaN 여부를 판단할 수 있게 되었다. 만일 폴리필이 필요하다면 아래와 같이 사용할 수 있다.

 

if (!Number.isNaN) {
  Number.isNaN = function(n) {
    return n !== n;
  }
}

const x = "foo";
Number.isNaN(x); // false

 

 - 위 코드는 NaN이 자기 자신과도 동등하지 않은 특성을 사용하여 간단하게 폴리필을 구현한 것이다.

 

* 무한대

- 이번에 알마볼 특수 숫자는 무한대이다. 자바스크립트에서는 0으로 나눌 경우 에러를 출력하지 않고 Infinity라는 결과값이 나온다. 음수의 경우 -Infinity 를 볼 수 있다. 나누기 외에도 자바스크립트는 유한 숫자 표현식(IEEE 754 부동 소수점)을 사용하므로 덧셈과 뺄셈에서도 무한대를 볼 수 있다.

 

 - IEEE 754 명세에 따르면 연산 결과가 너무 커서 표현하기 곤란할 때 Round-To-Nearest 모드가 결괏값을 정한다. 이를 통해 무한대를 확인해보기 위해 자바스크립트가 표현할 수 있는 가장 큰 숫자인 2^1024를 활용해보자. 

 

const x = Number.MAX_VALUE; // 1.7976931348623157e+308
x + x; // Infinity
x + Math.pow(2, 970); // Infinity
x + 999; // 1.7976931348623157e+308

 

 - 2^970은 무한대에 가깝기 때문에 올림처리 되었고, 그 아래의 숫자는 버림처리되었다

 

 - 하지만 한번 무한이 된 숫자는 그 어떤 숫자를 연산시키더라도 무한에서 돌아오지 못한다. 게다가 무한을 무한으로 나누면 자바스크립트에서 정의된 연산이 아니기때문에 NaN 이 결과값으로 나오게 된다.

 

 - 그러나 양수를 무한대로 나누면 0이 된다.

 

* 0 (Zero)

 - 위의 글에 이어 음수를 무한대로 나누면 어떻게 될까? 놀랍게도 -0 이다. 이는 비단 무한대에서만 일어나는 일이 아니다.

 

const x = 0 / -10 // -0
const y = 0 * -20 // -0

 

 - 덧셈과 뺄셈에는 -0이 나오지 않는다.(IE는 아예 나오지 않는다...)

 

 - 대체 -0은 무엇일까. 이는 문자열로 바꿀경우 "0" 이 되긴 한다. 심지어 비교연산에서도 0과 같다는 결과가 나온다. -0은 값의 크기로 어떤 정보와 그 값의 부호로 또 다른 정보를 동시에 나타내야 하는 경우 사용한다. 어떤 변숫값이 0에 도달하여 부호가 바뀌는 순간, 그 직전까지 이 변수의 이동방향이 필요하다는 의미이다. 즉, 잠재적인 정보소실을 방지하기 위해 부호가 있는 것이다.

 

- 지금까지 봐왔던 NaN, 0, -0 등을 비교하는 특이한 동등 비교 상황에는 Object.is 를 사용하면 좋다. 

 

const x = 10 / "foo"; // NaN
const y = -10 * 0; // -0

Object.is(x, NaN); // true
Object.is(y, -0); // true
Object.is(y, 0); // false

 

2. 레퍼런스

* JS의 레퍼런스

 - 다른 언어세서 값은 사용하는 구문에 따라 값-복사(Value-Copy) 또는 레퍼런스-복사(Reference-Copy)의 형태로 할당/전달 된다. 레퍼런스는 포인터의 특수한 형태로 다른 변수의 포인터를 가지게 된다.

 

 - 자바스크립트는 포인터라는 개념이 없다. 변수가 다른 변수를 참조하는 개념자체가 없다. 자바스크립트에서 서로 다른 10개의 레퍼런스가 있다면 이들은 저마다 단일 값을 개별적으로 참조하는 형태이다. 자바스크립트는 값의 타입만으로 값-복사, 레퍼런스-복사가 결정되는 언어이다.

 

let x = 10;
let y = a; // y는 x에서 '값'을 '복사'한다.
y++
x; // 10
y; // 11

const a = [1, 3, 5];
const b = a; // b는 공유된 [1, 3, 5] 의 레퍼런스이다.
b.push(7);
a; // [1, 3, 5, 7]
b; // [1, 3, 5, 7]

 

* 레퍼런스와 인자

 - 함수의 인자는 자바스크립트 레퍼런스 개념에서 헷갈리는 부분 중 하나이다.

 

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

const a = [1, 3, 5];
foo(a);
a; // [1, 3, 5, 7]

 

 - a를 인자로 넘기면 a의 레퍼런스 사본이 x에 할당된다. x와 a 모두 [1, 3, 5] 를 가리키는 별도의 레퍼런스다. push를 사용하여 값 자체를 변경하였다. 그 후, x에 새 값을 할당하더라도 초기 레퍼런스 a가 참조하고 있던 값에는 영향이 없게 된다.

 

 - 만일 a에 변함이 없게 하기 위해 값-복사에 의한 전달을 하려면 손수 값의 사본을 만들어 전달한 레퍼런스가 원본을 가리키지 않게 하면 된다. 위의 경우 [...a] 와 같이 사본을 만들어 전달할 수 있겠다. 그렇게 하면 a는 여전히 [1, 3, 5] 일 것이다.

 

 - 그렇다면 만약 스칼라 원시 값을 레퍼런스처럼 반영되도록 넘기려면 어떻게 해야할까. 이럴 경우에는 원시 값을 다른 합성 값(객체, 배열 등)으로 감싸면 된다.

 

 - 같은 원리로 숫자 값을 레퍼런스 형태로 넘기기위해 Number 객체 래퍼를 활용할 수도 있다. 하지만 Number 객체의 레퍼런스 사본이 함수에 전달되는 것은 맞으나 예상대로 움직이지는 않는다.

 

function foo(x) {
  x = x + 10;
  x; // 20
}

let n = 10;
let m = new Number(n); // Object(n) 으로 써도 된다.
typeof m // object

foo(m);
console.log(m) // 10

 

 - 위를 보면 스칼라 원시값인 10을 Number 객체로 박싱하고 함수에 넘겼으나 변화가 없다. 이유는 함수 내부에 x + 1 에서 원시 값 10이 Number 객체에서 자동 언박싱 되기 때문이다. 결국 결과값은 원시값 20 이다.

 

 - 굳이 위의 결과를 변화되도록 하려면 Number 객체에 프로퍼티를 추가하는 방법이 있다. 하지만 좋은 습관은 아니다. 차라리 위에서 언급했듯 다른 합성 값을 사용하는 것을 권장 한다.

 

 


참고

 

 해당 포스팅은 You Don't Know JS 를 읽고 개인적으로 필요한 내용을 추가 및 정리한 글입니다.

 

 

링크

 

 

GitHub - getify/You-Dont-Know-JS: A book series on JavaScript. @YDKJS on twitter.

A book series on JavaScript. @YDKJS on twitter. Contribute to getify/You-Dont-Know-JS development by creating an account on GitHub.

github.com

 

 

You Don’t Know JS - YES24

모호하고 애매한 여덟 가지 자바스크립트 개념 길라잡이_You Don’t Know JS 시리즈웹 초창기 시절부터 자바스크립트는 사람들이 대화하듯 웹 콘텐츠를 소비할 수 있게 해준 기반 기술이었다. 20년

www.yes24.com

 

 

 

'Language > JavaScript' 카테고리의 다른 글

<JS> 스코프란  (0) 2021.12.29
<JS> 암시적 변환  (0) 2021.12.28
<ES2021> ES2021 Features  (0) 2021.10.25
<ES2020> ES2020 Features  (0) 2021.10.24
<JS> 제너레이터  (0) 2021.10.17