Programming/Javascript

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

알로그 2020. 3. 8. 14:15
반응형

YOU DON'T KNOW JS : Chapter3. Native

주로 쓰는 네이티브(내장함수)들은 아래와 같다.

  • String()
  • Number()
  • Boolean()
  • Array()
  • Object()
  • Function()
  • RegExp()
  • Date()
  • Error
  • Symbol()

 

자바의 String() 생성자와 비슷하며 다음 코드처럼 사용할 수 있다.

var s = new String( "Hello World!" );
console.log( s.toString() ); // "Hello World!"

 

생성자처럼 사용할 수 있지만 실제로 생성되는 결과는 다르다.

var a = new String( "abc" );

typeof a; // "object" ... not "String"

a instanceof String; // true

Object.prototype.toString.call( a ); // "[object String]"

String이 아니며 object로 원시값 'abc'를 감싼 객체 래퍼(wrapper)라고 볼 수 있다.

 

내부 [[Class]]

typeof가 'object'인 값에는 [[Class]] 라는 내부 프로퍼티가 존재한다.
직접 접근할 수는 없고 Object.prototype.toString()라는 메서드에 값을 넣어 호출할 수 있다.

Object.prototype.toString.call( [1,2,3] );            // "[object Array]"

Object.prototype.toString.call( /regex-literal/i );    // "[object RegExp]"

 

null과 undefined도 네이티브 생성자는 없지만 내부 [[Class]] 값은 확인할 수 있다.

Object.prototype.toString.call( null );            // "[object Null]"
Object.prototype.toString.call( undefined );    // "[object Undefined]"

 

그 밖에 원시 값은 해당 객체 래퍼로 '박싱' 과정을 거친다.

Object.prototype.toString.call( "abc" );    // "[object String]"
Object.prototype.toString.call( 42 );        // "[object Number]"
Object.prototype.toString.call( true );        // "[object Boolean]"

 

래퍼 박싱하기

원시 값에는 프로퍼티나 메소드가 없으므로 .length나 .toString() 함수를 쓰려면 객체 래퍼로 감싸줘야한다. 하지만 자바스크립트에서는 원시 값을 알아서 박싱하므로 아래와 같은 코드가 가능하다.

var a = "abc";

a.length; // 3
a.toUpperCase(); // "ABC"

직접 객체 형태로 써야 할 이유는 거의 없고 엔진이 알아서 박싱을 해주므로 new String(), new Number() 처럼 코딩하지말고 바로 원시값 "abc", 42 등으로 코딩하는 것이 좋다.

 

객체 래퍼의 함정

아래는 false 값을 기대했지만 객체가 truthy하기 때문에 반대의 결과를 볼 수 있다.

var a = new Boolean( false );

if (!a) {
    console.log( "Oops" ); // never runs
}

 

수동으로 원시 값을 박싱하려면 Object() 함수를 사용해라.

var a = "abc";
var b = new String( a );
var c = Object( a );

typeof a; // "string"
typeof b; // "object"
typeof c; // "object"

b instanceof String; // true
c instanceof String; // true

Object.prototype.toString.call( b ); // "[object String]"
Object.prototype.toString.call( c ); // "[object String]"

 

언박싱

객체 래퍼의 원시 값은 valueOf() 함수로 추출한다.

var a = new String( "abc" );
var b = new Number( 42 );
var c = new Boolean( true );

a.valueOf(); // "abc"
b.valueOf(); // 42
c.valueOf(); // true

 

네이티브, 나는 생성자다

배열, 객체, 함수, 정규식 값은 리터럴 형태로 생성하는 것이 일반적이며, 리터럴은 생성자 형식으로 만든 것과 동일한 종류의 객체를 생성한다.

그리고 앞서 언급한 것처럼 생성자는 가급적 쓰지 않는 편이 좋다.

 

Array()

var a = new Array( 1, 2, 3 );
a; // [1, 2, 3]

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

 

주의할 점은 인자를 숫자로 하나만 받으면 배열의 크기를 미리 정하는 기능으로 적용되므로 주의해야한다. ([3] 배열이 생성되는 것이 아님)

var a = new Array( 3 );

a.length; // 3
a; // [undefined * 3]

 

undefined 값으로 채워진 배열은 다음과 같이 생성할 수 있다.

var a = Array.apply( null, { length: 3 } );
a; // [ undefined, undefined, undefined ]

apply() 는 모든 함수에서 사용가능한 함수로 첫 번째 인자로 객체 바인딩을 받고 두 번째 인자로 인자의 배열로, 이 안에 포함된 원소들이 펼쳐져서 함수의 인자로 전달된다.

인덱스별로 객체에서 키를 가져오는데 객체 인자를 arr 이라고 한다면 arr[0], arr[1], arr[2]로 접근할 것이고 이는 객체에서 존재하지 않기 때문에 모두 undefined를 반환할 것이다.

 

Object(), Function(), RegExp()

Object(), Function(), RegExp() 생성자도 선택사항이며 사용하지 않는 것을 권장한다.

var c = new Object();
c.foo = "bar";
c; // { foo: "bar" }

var d = { foo: "bar" };
d; // { foo: "bar" }

var e = new Function( "a", "return a * 2;" );
var f = function(a) { return a * 2; };
function g(a) { return a * 2; }

var h = new RegExp( "^a*b+", "g" );
var i = /^a*b+/g;

위에서 생성자를 이용해서 사용하는 경우는 거의 없고 굳이 쓸 이유도 없다.
정규표현식은 리터럴 형식으로 쓰는 것을 적극 권장하고 성능상 이점도 있다.

다른 생성자와 달리 RegExp()는 동적으로 정의할 경우 의미있는 함수이다.

var name = "Kyle";
var namePattern = new RegExp( "\\b(?:" + name + ")+\\b", "ig" );

var matches = someText.match( namePattern );

new RegExp('패턴', '플래그') 형식으로 자주 사용된다.

 

Date() and Error()

Date()와 Error()는 리터럴 형식이 없으므로 다른 네이티브에 비해 유용하다.

if (!Date.now) {
    Date.now = function(){
        return (new Date()).getTime();
    };
}

 

Error() 생성자는 앞에 new가 있든 없든 결과는 같다.
보통 throw 연산자와 함께 사용된다.

function foo(x) {
    if (!x) {
        throw new Error( "x wasn't provided" );
    }
    // ..
}

 

Symbol()

Symbol은 ES6에서 생긴 새로운 원시값 타입이다.
주로 ES6의 특수한 내장 로직에 쓰기 위해 고안되었지만 얼마든지 Symbol을 정의할 수 있다.

Symbol.create, Symbol.iterator 식으로 함수 객체의 정적 프로퍼티로 접근할 수 있다.

obj[Symbol.iterator] = function(){ /*..*/ };

S

ymbol을 직접 정의하려면 Symbol() 네이티브를 사용한다. new를 붙이면 에러가 나므로 주의하자.

var mysym = Symbol( "my own symbol" );
mysym;                // Symbol(my own symbol)
mysym.toString();    // "Symbol(my own symbol)"
typeof mysym;         // "symbol"

var a = { };
a[mysym] = "foobar";

Object.getOwnPropertySymbols( a );
// [ Symbol(my own symbol) ]

Symbol은 객체가 아닌 단순한 스칼라 원시 값이다.

 

네이티브 프로토타입

내장 네이티브 생성자는 .prototype 객체를 가진다.
이를테면 모든 String 객체는 String.prototype 객체에 정의된 메소드에 접근할 수 있다.

프로토타입의 위임덕에 모든 문자열이 이 메서드를 같이 쓸 수 있다.

var a = " abc ";

a.indexOf( "c" ); // 3
a.toUpperCase(); // " ABC "
a.trim(); // "abc"

 

프로토타입은 디폴트다.

변수에 값이 할당되지 않은 상태에서 Function.prototype: 빈 함수, RegExp.prototype: 빈 정규식, Array.prototype: 빈배열은 모두 적당한 디폴트 값이다.

function isThisCool(vals,fn,rx) {
    vals = vals || Array.prototype;
    fn = fn || Function.prototype;
    rx = rx || RegExp.prototype;

    return rx.test(
        vals.map( fn ).join( "" )
    );
}

isThisCool();        // true

isThisCool(
    ["a","b","c"],
    function(v){ return v.toUpperCase(); },
    /D/
);                    // false

프로토타입으로 디폴트 값을 세팅하면 이점이 있다.
.prototype은 이미 생성되어 내장된 상태로 단 한번만 상태된다. 그러나 [], function(){} 등을 디폴트 값으로 사용하면 isThisCool이 호출될 때마다 디폴트 값을 다시 생성하고 그만큼 메모리, CPU가 낭비된다.

그리고 이후에 변경될 디폴트 값으로 Array.prototype을 사용하는 일이 없어야 한다.
vals 값은 읽기 전용이지만 vals 변수 자체를 수정하면 결국 Array.prototype 자체도 수정된다.

 

Review

자바스크립트는 원시 값을 감싸는 객체 래퍼, 네이티브를 제공한다.
따라서 trim과 concat등의 함수를 바로 편리하게 사용할 수 있다.
"abc"와 같은 단순 스칼라 원시값은 length나 prototype에 정의된 함수를 호출하면 자동으로 박싱하여 필요한 프로퍼티나 메소드를 사용할 수 있다.

반응형