Javacsript si Wierd - 1-3. Native-thumbnail

Javacsript si Wierd - 1-3. Native

Javascript의 native (contsructor)
402

Native

Native란 Javascript가 실행되는 환경에 종속되지 않는 ECMAScript 명세 내의 내장 객체들을 말한다. 아래의 내장 객체들은 비교적 자주 쓰이는 녀석들이다.

var a = "str";
var b = new String("str");

typeof a; // string
typeof b; // object
a == b; // true
a === b; // false
b instanceof String; // true
Object.prototype.toString.call(b); // "[object String]"
a.toString(); // str

Native constructor(네이티브 생성자) 함수를 활용하면 우리의 예상과는 조금 다른 결과가 나오게 된다. String이라기보다는 Object에 가까운 결과물이 생성되었으며, abstract equality(동등 연산자)로 비교하면 true가, strict equality(일치 연산자)로 비교하면 false가 나오는 것을 확인할 수 있다.

[[CLASS]] property

typeof의 결과가 object인 값에는 [[CLASS]]라는 내부 property가 추가된다. 이 property는 Object.prototype.toString() 메서드로 접근해야 한다. 그리고 그 결과는 위에서 본 것처럼 해당 값의 native constructor를 가리킨다. 물론 nullundefined는 native constructor가 없음에도 결과는 아래와 같다.

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

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

Wrapper object

Primitives에는 property나 메서드가 없어서 위의 예시처럼 toString() 메서드를 string이 사용하게 되면, Javascript는 마치 new String("str")을 호출한듯이 string literal을 object로 변환해준다. 이 객체는 임시로 생성한 것인데, String object의 메서드들을 상속받아서 property에 접근할 수 있게 해주는 역할을 한다. 후에 property 참조가 끝나면 이 객체는 다시 없어진다. 이렇게 property나 메서드에 접근하기 위해 자동으로 생성되는 object를 wrapper object(래퍼 객체)라고 부른다. 이 wrapper object를 개발자가 직접 사용할 일은 거의 없다.

이렇게 boxing된 wrapper object를 unboxing하는 메서드로 valueOf()가 있다.

var a = new String("str");

a.valueOf(); // "str"

Native constructor

직접 쓸 일은 거의 없지만, 쓴다고 했을 때 주의할 점들이 있다.

Array

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

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

var c = new Array(3);
c; // [empty x 3]

숫자를 여럿 전달하면 우리가 예상하는 대로 array가 return되지만, 숫자가 하나만 전달되면 예기치 않은 상황이 발생할 수 있어 주의가 필요하다.

그것뿐이겠는가?

var c = new Array(3);
var d = [undefined, undefined, undefined];

c.join("-"); // "--"
d.join("-"); // "--"

c.map(function(v, i){ return i }); // [empty x 3]
d.map(function(v, i){ return i }); // [0, 1, 2]

그렇다면 undefined 값으로 채워진 배열을 코드로 만들 수 있는 방법은 없는 걸까?

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

apply() 메서드가 처음 나왔으니 잠시 언급하고 넘어가자. apply는 이전에 나왔던 call과 매우 유사하다. callfn.call(thisArg[, arg1[, arg2[, ...]]])과 같은 형태였고 마치 thisArg.fn(arg1, arg2, ...)처럼 실행된다. fn.apply([thisArg[, argsArray]])는 parameter를 array의 형태로 받는다는 것만이 다르다. 즉, 마치 thisArg.fn(argsArray)처럼 실행된다(물론 굳이 따지자면 array에 있는 value들이 parameter로 하나하나씩 전달될 것이므로 정확히 말하자면 thisArg.fn(argsArray.join(", "))와 더 비슷한 형태라고 말해야 할 것이다).

그런데 여기서는 argsArray로 array가 아닌 object가 전달되었고, thisArgnull이 전달되었다.

우선 thisArg는 this binding을 위해서 쓰인 parameter라는 것을 짚고 넘어가자. 이 binding이 아무래도 상관없다면, null이나 undefined를 써준다. 실행은 fn(argsArray)처럼 될 것이다. 따라서 아래의 세 코드는 같은 결과값을 return한다.

[1, 2];
Array(1, 2);
Array.apply(null, [1, 2]);

그렇다면 관건은 두 번째 parameter로 전달된 {length: 3}일 것이다. array-like로 전달된 object는 array와는 좀 다름에도 여전히 아래의 (숨겨진) 코드를 통해 정상적으로 작동한다.

function apply(context, args) {
  for(var i = 0; i < args.length; i++) { 
    /* ... */ args[i];
  }
}

그러므로 args의 key로 전달된 length 덕분에, Array는 그 길이만큼 돌면서 undefined가 담긴 array를 만들어낸다. 물론, 아래와 같이 써도 같은 결과가 반환된다.

var b = Array.apply(null, Array(3));

참고로 Array constructor는 앞에 new가 없어도 결과값이 같다.

Object, Function

아주 특별한 이유가 없는 한 해당 native들은 constructor로 활용할 이유가 거의 없다.

RegExp

Regular expression(정규표현식) 역시 literal하게 정의할 것이 권장된다. Syntax도 쉽고 성능상으로 이점이 있다. Javascript engine이 실행되기 전에 regular expression을 미리 compile하고 caching하기 때문이다. 다만, "동적으로" regular expression을 정의할 때 constructor를 사용하면 좋다.

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

Date, Error

둘은 literal 형식이 존재하지 않아 constructor 형태로 자주 쓰인다.

Error constructor는 앞에 new가 없어도 결과값이 같다. error object는 read-only property인 stack에 현재 execution stack context(실행 스택 콘텍스트)를 담는 것이다. Execution stack context에는 debugging에 도움이 될 정보들이 담긴다.

Error constructor는 주로 throw와 함께 쓰인다.

function a(arg1) {
  if (!arg1) {
    throw new Error("there is no arg1");
  }
}

메시지만 보고싶을 때는 stack property의 message를 보는 것보단 toString() 메서드를 사용한다.

var a = new Error("there is no arg1");
a.toString(); // "Error: there is no arg1"

Symbol

ES6 때 만들어진 type으로, 객체의 key를 만들기 위해서 쓰인다. 특히 충돌 위험이 없는 유일한 property key를 만들 때 쓰인다. Parameter로 string을 전달한다.

const obj = {};

const mySymbol1 = Symbol("test");
const mySymbol2 = Symbol("test");
obj[mySymbol1] = "test1";
obj[mySymbol2] = "test2";

obj; // { [Symbol(test)]: "test1", [Symbol(test)]: "test2" }
mySymbol1; // Symbol(test)
mySymbol2; // Symbol(test)
obj[mySymbol1]; // "test1"
obj[mySymbol2]; // "test2"

위와 같이 같은 string을 넣어줘도 충돌 위험이 없는 유일한 property key이기 때문에 error가 나지 않는다. 또한 new를 붙여 native constructor 형태로 쓰면 유일하게 error가 나는 native다.

Symbol은 ES6의 특수 내장 로직에서 쓰이기 위해 고안된 녀석이다. Symbol의 실제 값을 보는 것은 불가능하다. 위의 코드처럼 Symbol(test)라고만 뜰뿐, 그 이상을 알 수는 없다. 그래서 보통은 symbol을 (실제로 private property는 아니지만) private property 혹은 특별한 property라는 것을 알리고자 할 때 사용한다. 하여 여태까지 그런 역할을 해왔던 변수 앞 underscore(_)들은 앞으로 symbol로 대체될 가능성이 있다.


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

참고 사이트 :
tcpschool: 전역 객체와 래퍼 객체
Javascript: how is 'new Array(4)' different from Array.apply(null, {length: 4})?