Javacsript si Wierd - 1-2. Value-thumbnail

Javacsript si Wierd - 1-2. Value

Javascript의 Value에 대한 주의점들
383

Value

Array

구멍난sparse 배열은 유의할 점이 있다.

var a = [];

a[0] = 1;
a[2] = [3];

a[1]; // undefined

a.length; // 3

또한 array는 object의 하위 타입이라 key - property 문자열을 추가할 수 있다.

var a = [];

a[0] = 1;
a["example"] = "hello";

a.length; // 1
a["example"]; // "hello"
a.example; // "hello"

length가 증가하지 않음에 유의하자. 다만 key로 들어간 문자열이 10진수로 변환이 되는 값이면 문자열은 숫자처럼 인식이 된다.

var a = [];

a["26"] = "hi";

a.length; // 27

array-like object

Array-like object(유사 배열)의 값을 진짜 배열로 바꾸고 싶을 수 있다.

function changeToArray() {
  var arr = Array.prototype.slice.call(arguments);

  return arr;
}

changeToArray("a", "b", "c"); // ["a", "b", "c"]

Array.prototype으로부터 slice 메서드를 가져온다. call 함수는 다른 객체의 메서드를 parameter로 전달된 object가 본인의 메서드처럼 사용할 수 있도록 도와준다. 사용 방법은 fn.call(thisArg[, arg1[, arg2[, ...]]])처럼 사용하면 되고, 이렇게 되면 thisArg에 전달된 메서드 fn이 바로 호출된다. arg1, arg2 등이 주어지면 thisArg.fn(arg1, arg2)와 같이 동작한다. 따라서 위의 코드는 마치 아래와 같이 작동하는 것이다.

var arr = arguments.slice();

ES6부터는 아래와 같이 쓸 수도 있다.

function changeToArray() {
  var arr = Array.from(arguments);

  return arr;
}

changeToArray("a", "b", "c"); // ["a", "b", "c"]

String

string과 array를 생각해보면 둘은 닮은 점이 많다. length를 가져올 수 있고, index를 통해 접근할 수도 있다. concat 메서드도 사용 가능하다. 그래서 String도 Array-like object다.

물론 차이가 있다. String은 immutable(불변 값)이지만 Array는 mutable(가변 값)이다.

var a = "happy";
var b = ["h", "a", "p", "p", "y"];

a[4] = "Y";
b[4] = "Y";

a; // "happy"
b; // ["h", "a", "p", "p", "Y"]

따라서 String의 메서드들은 항상 새로운 값을 return하지만, 대부분의 Array 메서드는 그 즉시 기존의 array를 수정한다. 그래서 string의 유용한 메서드들을 array가 가져다 쓰는 것은 가능한 경우가 대부분이지만, string은 array의 메서드를 사실상 쓰기 어렵다.

var a = "foo";

a.join; // undefined
a.map; // undefined

var b = Array.prototype.join.call(a, "^");
var c = Array.prototype.map.call(a, function(v) {
  return v.toUpperCase() + ".";
}).join("");

b; // "f^o^o"
c; // "F.O.O" 

string이 array의 메서드를 쓰고 싶은 경우 split() 메서드를 사용해 아래와 같이 기교를 부린다.

var a = "string";
var b = a.split("").reverse().join("");

b; // gnirts;

Number

Javascript의 숫자 type은 Number뿐이다.

정수를 아래와 같이 이상하게 표현하는 것도 가능하다.

var a = 6.;

a; // 6

소수점 이하 값이 0이기에 가능한 표현이다.

큰 값은 Exponent form(지수형)으로 표현할 수 있다.

var a = 1E10;

typeof a; // number
a; // 10000000000
a.toExponential(); // "1e+10"

숫자 value는 Number object Wrapper로 Boxing할 수 있다. 따라서 Number.prototype 메서드들에 접근할 수 있다. 하지만 주의할 점이 있다.

77.toFixed(3); // SyntaxError

(77).toFixed(3); // "77.000"
77..toFixed(3); // "77.000"
77 .toFixed(3); // "77.000"
0.77.toFixed(3); // "0.770"

이것은 77.이 우선적으로 정수로 parsing되었기 때문이다.

다른 진법으로 숫자를 나타내는 것은 아래와 같다. 대표적인 binary(2진수), octal(8진수), hexadecimal(16진수)만 살펴보자.

var a = 0b11;
var b = 0o11;
var c = 0x11;

a; // 3
b; // 9
c; // 17

부동 소수점 숫자 문제

0.1 + 0.2 === 0.3; // false

IEEE 754 표준을 따르는 모든 언어에 상존하는 문제다. 이진 부동 소수점으로 표현된 0.1, 0.2, 0.3 모두 원래 숫자와 정확하게 일치하지 않는다. 이를 테면 0.3은 0.30000000000000004에 '가깝다'.

그렇다면 두 수의 비교는 어떻게 하면 좋을까?

function twoNumsCloseEnoughToEqual(num1, num2) {
  return Math.abs(num1 - num2) < Number.EPSILON;
}

var a = 0.1 + 0.2;
var b = 0.3;

twoNumsCloseEnoughToEqual(a, b); // true

미세한 반올림 오차를 tolerance(허용 공차)로 처리하는 코드다. 이런 미세한 오차는 machine epsilon이라고 불리는데, Javascript의 machine epsilon은 2의 -52승이다. ES6부터 Number.EPSILON 형태로 접근이 가능하다.

안전한 정수의 범위

표현된 값과 실제의 값이 일치한다고 확신할 수 있는 정수의 범위를 안전한safe 정수 범위라고 한다. Number.MAX_SAFE_INTEGER에서 Number.MIN_SAFE_INTEGER까지의 범위를 가지며 절대값은 9천조가 넘는다.

이와 같이 큰 숫자가 사용되는 경우는 주로 DB에서 64비트 id값을 설정하거나 할 때인데, 이런 한계 때문에 id값은 String으로 저장되어야 한다.

그리고 사실 비트 연산과 같이 숫자에만 가능한 연산이 있어 실제로 안전한 정수의 범위는 더 줄어들게 된다. 해당 값은 Math.pow(-2, 31)에서 Math.pow(2, 31) -1까지이다. 이 값들의 절대값은 대략 21억 전후의 숫자다.

Special values

undefined

sloppy mode에서는 undefined라는 식별자에 값을 할당하는 것이 가능하다. 물론 strict mode에서는 TypeError가 발생한다.

function fn1() {
  undefined = 1;
}

fn1();

function fn2() {
  "use strict";
  undefined = 1; // TypeError
}

fn2();

그러나 두 mode에서 undefined라는 지역 변수는 만들 수 있다.

var undefined = 1;

또한 void 연산자를 가지고 undefined를 return할 수 있다. 드물게 어떤 표현식의 return 값이 없음을 명시하기 위해 사용한다.

function doSomething() {
  if (!App.ready) {
    return void setTimeout(doSomething, 1000);
  }

  var result;

  /* ... */

  return result;
}

if (doSomething()) {
  /* ... */
}

setTimeout 함수는 타이머 취소 시 사용되는 고유 식별자를 return하기 때문에 이를 void로 undefined로 바꾸어주어 false positive(긍정 오류)가 없도록 구성했다. 물론 일반적으로는 아래와 같이 쓰는 것을 더 선호한다.

if (!App.ready) {
  setTimeout(doSomething, 1000);
  return;
}

NaN

var a = 1 / "string";
var b = "string2";

a == NaN; // false
a === NaN; // false

isNaN(a); // true 
isNaN(b); // true
Number.isNaN(a); // true
Number.isNaN(b); // false

NaN은 그 어떤 NaN과도 같지 않다.

어쨌든 NaN인지 아닌지를 판단하기 위해 isNaN이 등장한다. 내장 전역 메서드인 isNaN()은 일반 string에 대해서도 true를 return하기 때문에 예상과 다른 결과가 나오는 경우가 있지만, 이를 보완하기 위해 ES6부터는 Number.isNaN()을 사용한다.

특이한 동등비교를 할 때 머리아프지 않도록 ES6는 Object.is() 메서드도 제공한다.

var a = 1 / "string";
var b = "string2";

Object.is(a, NaN); // true
Object.is(b, NaN); // false

Infinity

Javascript에는 0으로 나누는 연산이 정의가 잘 되어 있다.

var a = 1/0;
var b = -1/0;

a; // Infinity
b; // -Infinity

또한 IEEE 754 부동 소수점을 사용하다 보니(유한 숫자 표현식Finite Numeric Representations), 아주 큰 수의 연산의 결과는 Infinity나 -Infinity가 될 수 있다. IEEE 754의 명세에 따르면 연산 결과가 너무 큰 경우, 가장 가까운 수로 반올림을 해서 그걸 토대로 결과를 정한다.

var maxNum = Number.MAX_VALUE;

maxNum + Math.pow(2, 969); // 1.7976931348623157e+308
maxNum + Math.pow(2, 970); // Infinity

그 결과 첫 번째 연산은 숫자로 표현되었으나 두 번째 연산은 Infinity가 결과값이 되었다.

0, -0

0과 -0이 존재한다. 값의 음/양으로 어떤 변수값이 어디서부터 출발해 0으로 도달했는지를 나타내야 하는 애플리케이션(비디오 재생 방향 등)이 존재할 수 있기 때문에 그렇다.

var a = 0/-5; // -0
var b = 0/5; // 0

명세에 의하면 Number -0을 String으로 바꾸면 항상 "0"이다. 반대로 String "-0"을 Number로 바꾸면 항상 -0이다.

var a = 0/-5;

a.toString(); // "0"
a + ""; // "0"
String(a); // "0"
JSON.stringify(a); // "0"

+"-0"; // -0
Number("-0"); // -0
JSON.parse("-0"); // -0

0과 -0도 같다고 인식한다.

0 === -0; // true

function isNegZero(num) {
  num = Number(num);
  return (num === 0) && (1/num === -Infinity);
}

물론 NaN에서와 마찬가지로 ES6부터 Object.is()를 사용할 수 있다.

var a = 0/-5;

Object.is(a, 0); // false
Object.is(a, -0); // true

Value & Reference

Javascript에서는 copy-by-value와 copy-by-reference만 존재한다. Pointer는 없다. 그리고 Primitives들은 전부 copy-by-value 방식을 따르고, object만이(하지만 function과 array 등이 같이 있는) copy-by-reference 방식을 따른다.

copy-by-value와 copy-by-reference는 무엇일까? copy-by-value는 어떤 값을 복사해올 때 memory에 담긴 value를 복사해온다는 뜻이다.

var a = 10;
var b = a;
b = 20;

a; // 10
b; // 20

할당이 이루어지는 시점에서 memory에 저장된 a의 value를 가져와서 b에 할당했기 때문에 b를 건드려도 a가 변화하지 않는다.

copy-by-reference는 어떨까?

var a = [1, 2, 3];
var b = a;
b.push(4);

a; // [1, 2, 3, 4]
b; // [1, 2, 3, 4]

할당이 이루어지는 시점에서 참조한 a, 그러나 여기서 참조한 것은 value가 아닌 주소(reference)에 존재하는 value였기 때문에 b에 변화가 생기면 a에도 변화가 생기게 된다. 이것을 copy-by-reference라고 한다.

둘 다 value를 가져온 거 아닌가요? 무슨 차이죠? copy-by-value는 a의 주소에 가서 value만 복사해온 뒤 b에 할당했다. 그 뒤로 b에 대한 작업을 할 때, 다시는 a를 찾아뵙지 않는다. 반면 copy-by-reference는 a의 "주소에 있는" value를 복사해 b에 할당한 것이므로, b에 할당된 해당 value가 존재하는 이상 변경 작업이 이루어지면 a의 주소에 가서 거기에 존재하는 value에 대해 변경 작업을 실행하는 것이다.

그러나 주소 자체를 복사해오는 pointer와는 다르다.

var a = [1, 2, 3];
var b = a;

b = [4, 5, 6];

a; // [1, 2, 3]
b; // [4, 5, 6]

만일 b가 pointer였다면, a의 값도 바뀌었겠지만 copy-by-reference 방식을 따르므로 a의 값이 변하지 않았다는 것을 확인할 수 있다. b가 더 이상 a 주소에 있는 값이 아닌 다른 값이 할당되자, copy-by-reference는 a에 대한 참조를 그만둔다. Pointer였다면 ba의 주소를 계속해서 참조했을 것이므로 a의 값도 같이 변했을 것이다.

object에 copy-by-value 방식을 적용하고자 한다면 shallow copy(얕은 복사)를 실행해야 한다. 이럴 때는 parameter 없이 실행되었을 때 shallow copy를 실행하는 slice() 메서드를 사용하면 좋다.

b = a.slice();

Primitives에 copy-by-reference를 적용하고 싶다면, Primitives를 object(혹은 array 등)로 감싸야 한다.

function changeValue(wrapper) {
  wrapper.value = 100;
}

var obj = {
  value: 1
}

changeValue(obj);

obj.value; // 100

참고 서적 :
카일 심슨, 2017, 한빛미디어, 『YOU DON'T KNOW JS: 타입과 문법, 스코프와 클로저』

참고 사이트 :
포인터(Pointer)와 레퍼런스(Reference : 참조자)의 차이