Programming/Javascript

[Javascript] You Don't Know JS (강제변환)

알로그 2020. 3. 8. 16:17
반응형

You Don't Know JS, Chapter 4. 강제변환

4.1 값 변환

값을 바꾸는 과정이 명시적이면 타입 캐스팅(Type Casting), 암시적이면 강제변환(Coercion)이라고 한다.

본 책에서는 암시적 강제변환(Explicit Coercion)과 명시적 강제변환(Implicit Coercion) 두 가지로 구분한다.

var a = 42;
var b = a + "";            // 암시적 강제변환
var c = String( a );    // 명시적 강제변환

두 방법 모두 42를 '42'로 바꾸는데, 핵심은 어떻게 변환할 것인가 하는 문제이다.

 

4.2 추상연산

값이 어떻게 문자열, 숫자, 불리언 등의 타입으로 변환되는지 알아보자.

 

4.2.1 ToString

문자열이 아닌 값 -> 문자열의 변환 작업은 ToString 추상 연산로직이 담당한다.
너무 작거나 큰 값은 지수 형태로 바뀐다.

// multiplying `1.07` by `1000`, seven times over
var a = 1.07 * 1000 * 1000 * 1000 * 1000 * 1000 * 1000 * 1000;

// seven times three digits => 21 digits
a.toString(); // "1.07e21"

배열도 기본적으로 재정의된 toString() 함수가 있다.

var a = [1,2,3];
a.toString(); // "1,2,3"

JSON 문자열화

ToString은 JSON.stringfy() 함수를 사용하여 JSON 문자열로 직렬화하는 문제와도 관련있다.

JSON.stringify( 42 );    // "42"
JSON.stringify( "42" );    // ""42"" (a string with a quoted string value in it)
JSON.stringify( null );    // "null"
JSON.stringify( true );    // "true"

JSON.stringfy()는 인자가 undefined, 함수, symbol 값이면 자동으로 누락시키며 배열에 포함되어 있으면 null로 변경하고, 객체 프로퍼티에 있으면 지워버린다.

JSON.stringify( undefined );                    // undefined
JSON.stringify( function(){} );                    // undefined

JSON.stringify( [1,undefined,function(){},4] );    // "[1,null,null,4]"
JSON.stringify( { a:2, b:function(){} } );        // "{"a":2}"

부적절한 JSON 값이나 객체 값을 문자열화 하려면 보통 toJSON() 메소드를 따로 정의해야 한다.

var o = { };

var a = {
    b: 42,
    c: o,
    d: function(){}
};

// create a circular reference inside `a`
o.e = a;

// would throw an error on the circular reference
// JSON.stringify( a );

// define a custom JSON value serialization
a.toJSON = function() {
    // only include the `b` property for serialization
    return { b: this.b };
};

JSON.stringify( a ); // "{"b":42}"

toJSON() 함수는 JSON 문자열로 바꾸는 것이 아닌 문자열화하기 적당한 JSON 안전 값으로 바꾸는 것이다.

 

4.2.2 ToNumber

숫자 아닌 값 -> 숫자 변환로직은 ToNumber 추상연산에 정의되어 있다.
true는 1, false는 0, undefined는 NaN, null은 0으로 바뀐다.

문자열 값에 ToNumber를 적용하면 대부분 숫자 리터럴로 변환되며, 실패하면 결과는 NaN이다.

객체는 동등한 원시 값으로 변환 후 그 결과를 ToNumber 규칙에 의해 강제변환한다.
해당 객체가 valueOf 메서드를 구현했는지, 반환 값이 원시값이면 그대로 강제변환하되, 그렇지 않을 경우 toString() 메서드가 존재하면 이를 이용하여 강제변환한다.

다음 코드를 참조하자.

var a = {
    valueOf: function(){
        return "42";
    }
};

var b = {
    toString: function(){
        return "42";
    }
};

var c = [4,2];
c.toString = function(){
    return this.join( "" );    // "42"
};

Number( a );            // 42
Number( b );            // 42
Number( c );            // 42
Number( "" );            // 0
Number( [] );            // 0
Number( [ "abc" ] );    // NaN

 

4.2.3 ToBoolean

흔히들 1과 0이 각각 true, false에 해당한다고 생각하는데, 자바스크립트에서는 숫자는 숫자고 불리언은 불리언 서로 별개이다.

자바스크립트의 모든 값은 다음 둘 중 하나이다.

  1. 불리언으로 강제변환하면 false가 되는 값
  2. 1번을 제외한 나머지, true가 되는 값

명세가 정의한 'falsy' 목록은 다음과 같다.

  • undefined
  • null
  • false
  • +0, -0, NaN
  • ""

이게 전부이며, 이 목록에 해당하는 값은 불리언으로 변환하면 false이다.

Falsy 객체

falsy 값을 둘러싼 객체를 형 변환한 값은 true이다.

var a = new Boolean( false );
var b = new Number( 0 );
var c = new String( "" );

var d = Boolean( a && b && c );
d; // true

truthy 값

falsy 목록에 없으면 무조건 truthy값이다.

var a = "false";
var b = "0";
var c = "''";

var d = Boolean( a && b && c );

d;

위에서 true로 반환되는 이유는 문자열 값 자체는 모두 truthy 값이다.

위와 마찬가지 예제를 보자. 이번에도 true이다.

var a = [];                // empty array -- truthy or falsy?
var b = {};                // empty object -- truthy or falsy?
var c = function(){};    // empty function -- truthy or falsy?

var d = Boolean( a && b && c );

d; //true

 

4.3 명시적 강제변환

확실한 타입변환으로 개발자들이 흔히 사용하는 타입 변환은 대게 이 명시적 강제변환 범주에 속한다.

4.3.1 명시적 강제변환 : 문자열 <-> 숫자

다음은 두 타입 간 명시적 강제변환 예제이다.

var a = 42;
var b = String( a );

var c = "3.14";
var d = Number( c );

b; // "42"
d; // 3.14

또는 암시적 강제변환 같지만 명시적인 변환의 다른 예이다.

var a = 42;
var b = a.toString();

var c = "3.14";
var d = +c;

b; // "42"
d; // 3.14

원시값 42에는 toString() 메서드가 없지만 엔진은 toString()을 사용할 수 있게 자동으로 42를 객체 래퍼로 '박싱'한다. (명시적이면서 암시적인 작동)
오픈소스 커뮤니티에서 + 단항 연산자를 이용한 방법은 명시적 강제변환으로 인정하는 분위기이다

날짜 -> 숫자

  • 단향연산자는 Date 객체를 숫자로 강제변환 할 때도 사용된다.
var d = new Date( "Mon, 18 Aug 2014 08:53:06 CDT" );

+d; // 1408369986000

현재 시각을 타임 스탬프로 바꿀 때, 관용적으로 다음과 같이 사용한다.

var timestamp = +new Date();

Date객체로부터 타임 스탬프를 얻는 방법은 오히려 강제 변환을 하지 않는 쪽이 더 명시적이라 권장한다.

var timestamp = new Date().getTime();
// var timestamp = (new Date()).getTime();
// var timestamp = (new Date).getTime();

현재 타임스탬프는 Date.now(), 그 외 특정 날짜, 시간은 new Date().getTime()을 사용하는 것을 권장한다.

이상한 나라의 틸드(~)

~(틸드)는 자바스크립트의 강제변환 연산자이자 가장 헷갈리는 연산자이다.
~ 연산자는 32비트 숫자로 강제변환 후 NOT 연산을 한다. (각 비트를 반대로 뒤집음)

indefOf()에 ~를 붙이면 어떤 값을 강제변환하여 불리언 값으로 적절하게 만들 수 있다.

var a = "Hello World";

~a.indexOf( "lo" );            // -4   <-- truthy!

if (~a.indexOf( "lo" )) {    // true
    // found it!
}

~a.indexOf( "ol" );            // 0    <-- falsy!
!~a.indexOf( "ol" );        // true

if (!~a.indexOf( "ol" )) {    // true
    // not found!
}

~은 indexOf() 검색 결과 실패시 -1을 flasy한 0으로 그 외에 truthy한 값으로 바꾼다.

비트 잘라내기

더블틸드(~~)를 사용할 수 있다.
ToInt32 강제변환을 두번 적용하는 결과와 같다. (Math.floor()와 결과가 다르니 착각하지 말 것)

Math.floor( -49.6 );    // -50
~~-49.6;                // -49

 

4.3.2 명시적 강제변환: 숫자 형태의 문자열 파싱

문자열에 포함된 숫자를 파싱하는 것은 문자열->숫자 강제변환과 비슷하지만 차이가 있다.

var a = "42";
var b = "42px";

Number( a );    // 42
parseInt( a );    // 42

Number( b );    // NaN
parseInt( b );    // 42

parseInt() 함수는 좌->우 방향으로 파싱하다가 숫자가 아닌 문자를 만나면 바로 멈춘다.
파싱은 강제변환의 대안이 될 수 없고, 목적 자체가 다르다.

반은 명시적이고 반은 암시적인 강제변환이 유용하다고 생각되는 케이스도 있다.

var a = {
    num: 21,
    toString: function() { return String( this.num * 2 ); }
};

parseInt( a ); // 42

parseInt()의 예제 몇가지를 더 살펴보자.

parseInt( 0.000008 );        // 0   ("0" from "0.000008")
parseInt( 0.0000008 );        // 8   ("8" from "8e-7")
parseInt( false, 16 );        // 250 ("fa" from "false")
parseInt( parseInt, 16 );    // 15  ("f" from "function..")

parseInt( "0x10" );            // 16
parseInt( "103", 2 );        // 2

 

4.3.3 명시적 강제변환: *-> 불리언

String(), Number()도 그렇듯 Boolean()은 명시적인 강제변환이다.

var a = "0";
var b = [];
var c = {};

var d = "";
var e = 0;
var f = null;
var g;

Boolean( a ); // true
Boolean( b ); // true
Boolean( c ); // true

Boolean( d ); // false
Boolean( e ); // false
Boolean( f ); // false
Boolean( g ); // false

+ 단항 연산자가 값을 숫자로 강제 변환하는 것처럼 ! 부정 단항 연산자도 값을 불리언으로 명시적으로 강제변환한다.
문제는 그 과정에서 truthy, falsy 값이 바뀌므로 일반적으로 두번 사용하는 !! 이중 부정 연산자를 사용한다.

var a = "0";
var b = [];
var c = {};

var d = "";
var e = 0;
var f = null;
var g;

!!a;    // true
!!b;    // true
!!c;    // true

!!d;    // false
!!e;    // false
!!f;    // false
!!g;    // false

삼항연산자 ? : 는 평가 결과에 따라 true 또는 false를 반환한다.

var a = 42;
var b = a ? true : false;

이러한 명시적으로 암시적(Explicitly implicit)이라 했는데, 사용하지 말자.
Boolean(a) 또는 !!a 같은 명시적 강제변환이 훨씬 좋다.

 

4.4 암시적 변환

암시적 강제변환은 명확하지 않게 숨겨진 형태로 일어나는 타입변환이다.

암시적 강제변환의 목적은 장황함(Verbosity), 보일러플레이트(Boilerplate), 불필요한 상세 구현을 줄이는 것이다.

 

4.4.1 암시적이란?

첫번째 라인보다 두번째 라인이 실제로 코드 가독성을 높이고 세세한 구현부를 추상화하거나 감추는데 도움이 될 수 있다.

SomeType x = SomeType( AnotherType( y ) ) // As-is

SomeType x = SomeType( y ) // To-be

 

4.4.2 암시적 강제변환: 문자열 <-> 숫자

암시적 강제변환을 일으키는 몇몇 연산의 의미를 알아보자.
+ 연산자는 숫자의 덧셈, 문자열 접합 두 가지 목적으로 오버로드 될 수 있다.

var a = "42";
var b = "0";

var c = 42;
var d = 0;

a + b; // "420"
c + d; // 42
var a = [1,2];
var b = [3,4];

a + b; // "1,23,4"

명시적 강제변환 String(a)에 비해 암시적 강제변환 a + "" 에서 유의할 점이 있다.

a + ""는 a 값을 valueOf() 메서드에 전달하여 호출하고, 그 결과는 ToString 추상 연산을 통해 최종 문자열로 변환되는 반면에 String(a)는 toString()을 직접 호출한다.

var a = {
    valueOf: function() { return 42; },
    toString: function() { return 4; }
};

a + "";            // "42"

String( a );    // "4"

자바스크립트에서는 암시적 강제변환을 훨씬 더 많이 쓴다.

 

4.4.3 암시적 강제변환: 불리언 -> 숫자

onlyOne 함수는 인자 중 하나만 true/truthy인지 확인하는 함수이다.
불리언 값을 숫자로 변환해서 계산하는 암시적 강제변환의 예이다.

function onlyOne() {
    var sum = 0;
    for (var i=0; i < arguments.length; i++) {
        // skip falsy values. same as treating
        // them as 0's, but avoids NaN's.
        if (arguments[i]) {
            sum += arguments[i];
        }
    }
    return sum == 1;
}

var a = true;
var b = false;

onlyOne( b, a );        // true
onlyOne( b, a, b, b, b );    // true

onlyOne( b, b );        // false
onlyOne( b, a, b, b, b, a );    // false

명시적 강제변환 버전이다.

function onlyOne() {
    var sum = 0;
    for (var i=0; i < arguments.length; i++) {
        sum += Number( !!arguments[i] );
    }
    return sum === 1;
}

 

4.4.4 암시적 강제변환: * -> 불리언

if문, 반복문 내부 조건, 삼항연산, ||와 &&의 좌측 피연산자에서 일어난다.

var a = 42;
var b = "abc";
var c;
var d = null;

if (a) {
    console.log( "yep" );        // yep
}

while (c) {
    console.log( "nope, never runs" );
}

c = d ? a : b;
c;                    // "abc"

if ((a && d) || c) {
    console.log( "yep" );        // yep
}

 

4.4.5 &&와 ||연산자

다른 언어와 달리 자바스크립트에서는 두 연산자의 결과값이 논리 값이 아니다.

var a = 42;
var b = "abc";
var c = null;

a || b;        // 42
a && b;        // "abc"

c || b;        // "abc"
c && b;        // null

|| 연산자는 그 결과가 true면 첫 번째 피연산자 값을, false면 두 번째 피연산자 값을 반환한다.
&&는 그 반대로 동작한다.

이런 특성을 잘 활용한 예제이다.

function foo(a,b) {
    a = a || "hello";
    b = b || "world";

    console.log( a + " " + b );
}

foo();                    // "hello world"
foo( "yeah", "yeah!" );    // "yeah yeah!"

그렇다면 기존의 소스코드는 어떤식으로 동작하는 것일까?

var a = 42;
var b = null;
var c = "foo";

if (a && (b || c)) {
    console.log( "yep" );
}

a && (b || c) 표현식의 결과는 true가 아닌 'foo'다.
if문은 이 'foo'를 불리언 타입으로 강제변환하여 true로 만드는 것이다.

 

4.4.6 심벌의 강제변환

심벌 -> 문자열 명시적 강제변환은 허용되나 암시적 강제변환은 금지된다.

var s1 = Symbol( "cool" );
String( s1 );                    // "Symbol(cool)"

var s2 = Symbol( "not cool" );
s2 + "";                        // TypeError

 

4.5 느슨한/엄격한 동등비교

동등함의 비교시 ==는 강제변환을 허용하지만, ===는 강제변환을 허용하지 않는다.

 

4.5.1 비교성능

강제변환 시 처리 시간이 약간 더 소요되지만, 몇 마이크로초 단위의 차이이다.

 

4.5.2 추상 동등 비교

  • NaN은 그 자신과도 동등하지 않다
  • +0과 -0은 동등하지 않다.
var a = 42;
var b = "42";

a === b;    // false
a == b;        // true

boolean 타입과 비교할 때는, boolean을 ToNumber()값에 넣어서 비교하기 때문에 주의해야 한다.

var a = "42";
var b = true;

a == b;    // false

null과 undefined를 느슨한 동등비교(==)를 하면 서로에게 타입을 맞춘다.

var a = null;
var b;

a == b;        // true
a == null;    // true
b == null;    // true

a == false;    // false
b == false;    // false
a == "";    // false
b == "";    // false
a == 0;        // false
b == 0;        // false

객체의 경우 ToPrimitive() 결과를 통해 비교한다.

var a = 42;
var b = [ 42 ];

a == b;    // true

null과 undefined의 경우는 객체 래퍼가 따로 없으므로 예외이다.

 

4.5.3 희귀사례

"0" == false;            // true -- UH OH!
false == 0;                // true -- UH OH!
false == "";            // true -- UH OH!
false == [];            // true -- UH OH!
"" == 0;                // true -- UH OH!
"" == [];                // true -- UH OH!
0 == [];                // true -- UH OH!

암시적 강제변환을 안전하게 사용하려면 다음을 명심하자.

  • 피연산자 중 하나가 true/false일 가능성이 있으면 절대 == 연산자를 쓰지 마라.
  • 피연산자 중 하나가 [], "", 0이 될 가능성이 있으면 가급적 == 연산자를 쓰지 마라.

 

4.6 추상관계 비교

var a = [ 42 ];
var b = [ "43" ];

a < b;    // true
b < a;    // false

다음은 둘다 문자열로 강제변환 되기 때문에 첫 문자인 0은 4보다 작으므로 false 이다.

var a = [ "42" ];
var b = [ "043" ];

a < b;    // false

위의 경우와 동일한 로직이다.

var a = [ 4, 2 ];
var b = [ 0, 4, 3 ];

a < b;    // false

이상한 경우를 살펴보자

var a = { b: 42 };
var b = { b: 43 };

a < b;    // false
a == b;    // false
a > b;    // false

a <= b;    // true
a >= b;    // true

a < b, a == b는 false인데 a <= b는 어떻게 true일까?
a <= b의 로직은 b < a 의 평가 결과를 부정하도록 명세에 기술되어 있기 때문이다.

 

4.7 정리하기

  • 강제변환의 작동원리를 명시적/암시적 두가지 유형으로 나누어 살펴봤다.
  • 강제변환은 욕을 많이 먹지만 상당히 유용한 기능이다.
  • 명시적 강제변환은 의도가 확실하며 코드 가독성 및 유지보수성의 장점이 있다.
  • 암시적 강제변환은 숨겨진 로직에 의한 부수 효과가 있어서 별로라고 생각할 수 있지만 코드 가독성을 향상시키는 경우도 있다.
반응형