Programming/Javascript

[Javascript] You Don't Know JS (Values)

알로그 2020. 2. 22. 23:01
반응형

YOU DON'T KNOW JS : Chapter2. Values

Arrays

타입이 엄격한 언어들과 달리 자바스크립트의 배열은 문자열, 숫자, 객체 심지어 배열까지 모든 타입의 값을 포함할 수 있는 컨테이너이다.

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

a.length;        // 3
a[0] === 1;        // true
a[2][0] === 3;    // true

 

배열의 사이즈를 미리 정할 필요가 없으며, 선언하고나서도 값을 추가할 수 있다.

var a = [ ];

a.length;    // 0

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

a.length;    // 3

Warning: 배열에서 delete 연산자를 사용하면 배열안에 있는 요소를 지우게 된다.

그러나 배열의 마지막 요소까지 지우더라도 length 속성은 변하지 않으므로 주의해야 한다.
(delete 연산자에 대해선 Chapter5에서 다룰 예정)

 

중간에 누락되는 배열생성은 주의해야 한다.

var a = [ ];

a[0] = 1;
// no `a[1]` slot set here
a[2] = [ 3 ];

a[1];        // undefined

a.length;    // 3

 

a[1]은 선언되지 않았지만 a[1]=undefined값과 동일하게 적용된다.

배열도 object이므로 문자열키와 속성을 추가할 수 있다.(length의 속성을 증가시키진 않음)

var a = [ ];

a[0] = 1;
a["foobar"] = 2;

a.length;        // 1
a["foobar"];    // 2
a.foobar;        // 2

 

주의해야 할 점은 만약 숫자 값을 문자열로 입력하는 경우 10진수 값으로 변경되어 적용될 수 있다.

var a = [ ];
a["13"] = 42;
a.length; // 14

일반적으로 문자열 키/속성을 배열에 추가하는 것은 좋은 생각이 아니다.
키/속성에서 값을 저장하기 위해 object를 사용하고 배열에는 엄격하게 숫자로 인덱스를 사용해라.

 

 

Array-Likes

배열과 같은 값(숫자 인덱스 값 모음)을 실제 배열로 변환해야 하는 경우가 있을 수 있다.
일반적으로 배열 함수(indexOf (..), concat (..), forEach ( ..))를 호출한다.

예를 들어, 다양한 DOM 쿼리 작업은 배열은 아니지만 변환 목적으로 배열과 유사한(배열이 아닌) DOM 요소의 목록을 반환한다.

 

이러한 변환을 수행하는 가장 일반적인 방법 중 하나는 slice() 함수를 사용하는 것이다.

function foo() {
    var arr = Array.prototype.slice.call( arguments );
    arr.push( "bam" );
    console.log( arr );
}

foo( "bar", "baz" ); // ["bar","baz","bam"]

 

ES6에서는 Array.from() 내장함수를 사용할 수 있다.

var arr = Array.from( arguments );

 

String

문자열은 본질적으로 문자의 배열일 뿐이라고 생각하는 것이 일반적이다.
JavaScript 문자열은 실제로 문자 배열과 동일하지 않음을 인식해야한다.

예를 들어 다음 두 값을 고려해보자.

var a = "foo";
var b = ["f","o","o"];

 

문자열은 배열과 유사하다.

둘 다 length 속성, indexOf 함수(ES5의 경우에만 배열 버전) 및 concat 함수를 사용할 수 있다.

a.length;                            // 3
b.length;                            // 3

a.indexOf( "o" );                    // 1
b.indexOf( "o" );                    // 1

var c = a.concat( "bar" );            // "foobar"
var d = b.concat( ["b","a","r"] );    // ["f","o","o","b","a","r"]

a === c;                            // false
b === d;                            // false

a;                                    // "foo"
b;                                    // ["f","o","o"]

 

둘 다 기본적으로 "문자의 배열"이라고 할 수 있나? 아니다.

a[1] = "O";
b[1] = "O";

a; // "foo"
b; // ["f","O","o"]

 

JavaScript 문자열은 변경이 불가능하지만 배열은 변경 가능하다.

또한 a[1] 액세스 양식이 항상 자바스크립트 엔진에서 유용한 것은 아니다.
이전 버전의 IE는 해당 구문을 허용하지 않았다. (현재는 가능)

올바른 접근 방식은 a.charAt (1) 이다.

문자열은 불변 값이므로 새로운 문자열을 생성한 후 반환하고, 대부분 배열 메소드는 바로 원소를 수정한다.

c = a.toUpperCase();
a === c;    // false
a;            // "foo"
c;            // "FOO"

b.push( "!" );
b;            // ["f","O","o","!"]

 

또한 문자열을 다룰 때 도움이 될 수 있는 많은 배열 메소드를 실제로 사용할 수는 없지만 문자열에 대해 non-mutaion 배열 함수를 빌려 쓸 수 있다.

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

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

c;                // "f-o-o"
d;                // "F.O.O."

 

문자열을 뒤집는 예를 살펴보자. (JavaScript 인터뷰 퀴즈 질문으로 자주 등장함)
배열에는 reverse () 메소드가 존재한다.

a.reverse;        // undefined

b.reverse();    // ["!","o","O","f"]
b;                // ["!","o","O","f"]

 

불행히도, 문자열에서는 배열함수를 빌려쓰는 것 또한 불가능하다.
문자열은 변경할 수 없기 때문에 바로 수정할 수 없기 때문이다.

Array.prototype.reverse.call( a );
// still returns a String object wrapper (see Chapter 3)
// for "foo" :(

 

또 다른 해결 방법 (aka hack)은 문자열을 배열로 변환하고 원하는 작업을 수행한 다음 문자열로 다시 변환하는 것이다.

var c = a
    // split `a` into an array of characters
    .split( "" )
    // reverse the array of characters
    .reverse()
    // join the array of characters back to a string
    .join( "" );

c; // "oof"

위의 방식이 이상하게 느껴지더라도 간단한 문자열에서 작동하므로 종종 그러한 접근 방식으로 작업을 수행한다.

문자열 작업이 빈번한 경우에는 관점을 바꿔서 문자열을 배열로 취급해서 사용하는 것이 더 나을 수도 있다. 문자열로 나타나야 할 때는 join() 함수를 호출하면 된다.

 

Numbers

대부분의 현대 언어와 마찬가지로 JavaScript 숫자의 구현은 "부동 소수점"이라고하는 "IEEE 754"표준을 기반으로 한다. JavaScript는 특히 표준의 "double precision"형식 (일명 "64 bit binary")을 사용한다.

 

Numeric Syntac

자바스크립트에서 숫자 리터럴은 10 진수 리터럴로 표현된다.

var a = 42;
var b = 42.3;

 

소수점 이하 0은 출력하지 않는다.

var a = 42.300;
var b = 42.0;

a; // 42.3
b; // 42

 

매우 크거나 아주 작은 숫자는 기본적으로 다음과 같이 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

 

toFixed () 메서드를 사용하면 값을 표시할 소수점 이하 자릿수를 지정할 수 있다.

var a = 42.59;

a.toFixed( 0 ); // "43"
a.toFixed( 1 ); // "42.6"
a.toFixed( 2 ); // "42.59"
a.toFixed( 3 ); // "42.590"
a.toFixed( 4 ); // "42.5900"

 

toPrecision ()은 비슷하지만 값을 나타내는 데 사용되는 유효 자릿수를 지정한다.

var a = 42.59;

a.toPrecision( 1 ); // "4e+1"
a.toPrecision( 2 ); // "43"
a.toPrecision( 3 ); // "42.6"
a.toPrecision( 4 ); // "42.59"
a.toPrecision( 5 ); // "42.590"
a.toPrecision( 6 ); // "42.5900"

 

위의 두 함수는 변수가 아닌 숫자 리터럴에서 바로 접근 가능하지만 주의해야한다.

// invalid syntax:
42.toFixed( 3 );    // SyntaxError

// these are all valid:
(42).toFixed( 3 );    // "42.000"
0.42.toFixed( 3 );    // "0.420"
42..toFixed( 3 );    // "42.000"

 

42.toFixed (3)은 잘못된 구문이며 42. 이 리터럴의 일부가 되어 toFixed 함수를 사용할 수 없기 때문이다.

큰 숫자는 지수형식으로 표시한다.

var onethousand = 1E3;                        // means 1 * 10^3
var onemilliononehundredthousand = 1.1E6;    // means 1.1 * 10^6

 

숫자 리터럴은 2진, 8진 및 16진과 같은 다른 진법으로 표현할 수 있다.

0xf3; // hexadecimal for: 243
0Xf3; // ditto

0363; // octal for: 243

 

ES6부터 다음과 같은 새로운 형식도 유효하다.

0o363;        // octal for: 243
0O363;        // ditto

0b11110011;    // binary for: 243
0B11110011; // ditto

0O363 양식은 사용하지 말자. 대문자 O 옆의 0은 혼란을 요구하므로 항상 소문자로 0x, 0b 및 0o를 사용하자.

 

Small Decimal Values

이진 부동 소수점 숫자를 사용하는 가장 유명한 부작용은 다음과 같다.
JavaScript뿐만 아니라 IEEE 754를 사용하는 모든 언어에 해당한다.

0.1 + 0.2 === 0.3; // false

간단히 말해서, 이진 부동 소수점에서 0.1과 0.2의 표현은 원래의 값과 정확히 일치하지 않는다.
또한 값을 더한 결과도 정확히 0.3이 아니며, 0.30000000000000004와 가깝다고 볼 수 있다.

 

그렇다면 어떻게 두 값을 비교해야 할까?

가장 일반적으로 허용되는 방법은 "반올림 오차"값을 비교 허용 오차로 사용하는 것이다.
이 작은 오차를 machine epsilon이라고하며, JavaScript는 일반적으로 2 ^ -52 (2.220446049250313e-16)다.

 

ES6부터는 이 값이 Number.EPSILON으로 정의되어 있다.

if (!Number.EPSILON) {
    Number.EPSILON = Math.pow(2,-52);
}

 

이 Number.EPSILON을 사용하여 두 숫자 (반올림 오차 허용 범위 내)를 비교할 수 있다.

function numbersCloseEnoughToEqual(n1,n2) {
    return Math.abs( n1 - n2 ) < Number.EPSILON;
}

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

numbersCloseEnoughToEqual( a, b );                    // true
numbersCloseEnoughToEqual( 0.0000001, 0.0000002 );    // false

 

표현할 수있는 최대 부동 소수점 값은 대략 1.798e + 308 이며, Number.MAX_VALUE로 미리 정의되어있다.
Number.MIN_VALUE는 대략 5e-324이며 음수가 아니지만 실제로는 0에 가깝다.

 

Safe Integer Ranges

숫자가 표현되는 방식으로 인해 안전값의 범위가 있으며, Number.MAX_VALUE보다 훨씬 작다.

'안전하게' 표현 될 수있는 최대 정수는 2 ^ 53-1이며 9007199254740991다.

이 값은 실제로 ES6에서 Number.MAX_SAFE_INTEGER로 자동 정의됩니다.
당연히 최소값은 ES6에 Number.MIN_SAFE_INTEGER로 정의되어 있다.

 

JS 프로그램이 이러한 많은 수를 처리하는 경우는 데이터베이스 등의 64 비트 ID를 처리 할 때가 대부분이다.
64 비트 숫자는 숫자 유형으로 정확하게 표현할 수 없으므로 string 타입으로 저장해야한다.

 

Testing for Integers

값이 정수인지 테스트하려면 ES6부터 Number.isInteger()를 사용할 수 있다.

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

 

ES6 이전 버전을 위한 폴리필은 다음과 같다.

if (!Number.isInteger) {
    Number.isInteger = function(num) {
        return typeof num == "number" && num % 1 == 0;
    };
}

 

값이 안전한 정수인지 테스트하려면 ES6 지정 Number.isSafeInteger()를 사용하자.

Number.isSafeInteger( Number.MAX_SAFE_INTEGER );    // true
Number.isSafeInteger( Math.pow( 2, 53 ) );            // false
Number.isSafeInteger( Math.pow( 2, 53 ) - 1 );        // true

 

ES6 이전 브라우저에서 Number.isSafeInteger()를 폴리 필하려면 :

if (!Number.isSafeInteger) {
    Number.isSafeInteger = function(num) {
        return Number.isInteger( num ) &&
            Math.abs( num ) <= Number.MAX_SAFE_INTEGER;
    };
}
반응형