소개

Intro

JavaScript 언어의 핵심에 대한 내용을 모아 JavaScript Garden을 만들어 었다. 이 글이 초보자가 JavaScript 익히면서 자주 겪는 실수, 미묘한 버그, 성능 이슈, 나쁜 습관들 줄일 수 있도록 도와줄 것이다.

JavaScript Garden은 단순히 JavaScript 언어 자체를 설명하려 만들지 않았다. 그래서 이 글에서 설명하는 주제들을 이해하려면 반드시 언어에 대한 기본 지식이 필요하다. 먼저 Mozilla Developer Network에 있는 문서로 JavaScript 언어를 공부하기 바란다.

저자들

이 글은 Stack Overflow에서 사랑받는 두 사람 Ivo WetzelZhang Yi Jiang의 작품이다. Ivo Wetzel이 글을 썼고 Zhang Yi jiang이 디자인을 맡았다.

기여자들

번역

호스팅

JavaScript Garden은 Github에서 호스팅하고 있고 Cramer DevelopmentJavaScriptGarden.info에서 미러링해주고 있다.

저작권

JavaScript Garden은 MIT license를 따르고 GitHub에서 호스팅하고 있다. 문제를 발견하면 이슈를 보고하거나 수정해서 Pull Request를 하라. 아니면 Stack Overflow 채팅 사이트의 Javascript room에서 우리를 찾으라.

객체

객체와 프로퍼티

JavaScript에서 nullundefined를 제외한 모든 것들은 객체처럼 동작한다.

false.toString(); // 'false'
[1, 2, 3].toString(); // '1,2,3'

function Foo(){}
Foo.bar = 1;
Foo.bar; // 1

숫자 리터럴은 객체처럼 사용되지 못할꺼라는 오해가 있는데 이것은 단지 JavaScript 파서의 문제일 뿐이다. JavaScript 파서는 숫자에 Dot Notation이 들어가면 오류라고 생각한다.

2.toString(); // SyntaxError가 난다.

하지만, 숫자를 객체처럼 사용할수 있는 꼼수가 몇 가지 있다.

2..toString(); // 두 번째 점은 잘 된다.
2 .toString(); // 왼쪽 공백이 있으면 잘 된다.
(2).toString(); // 2를 먼저 해석한다.

Object 타입

JavaScript 객체는 name/value 쌍으로 된 프로퍼티로 구성되기 때문에 Hashmap처럼 사용될 수도 있다.

객체 리터럴인 Object Notation으로 객체를 만들면 Object.prototype을 상속받고 프로퍼티를 하나도 가지지 않은 객체가 만들어진다.

var foo = {}; // 깨끗한 새 객체를 만든다.

// 값이 12인 'test' 프로퍼티가 있는 객체를 만든다.
var bar = {test: 12}; 

프로퍼티 접근

객체의 프로퍼티는 객체이름 다음에 점을 찍어(Dot Notation) 접근하거나 각괄호를 이용해(Square Bracket Notation) 접근할 수 있다.

var foo = {name: 'kitten'}
foo.name; // kitten
foo['name']; // kitten

var get = 'name';
foo[get]; // kitten

foo.1234; // SyntaxError
foo['1234']; // works

두 방식 모두 거의 동일하게 동작한다. 다만 차이가 있다면 각괄호 방식은 프로퍼티 이름을 동적으로 할당해서 값에 접근 할수 있지만 점을 이용한 방식은 구문 오류를 발생시킨다.

프로퍼티 삭제

객체의 프로퍼티를 삭제하려면 delete를 사용해야만 한다. 프로퍼티에 undefinednull을 할당하는 것은 프로퍼티를 삭제하는 것이 아니라 프로퍼티에 할당된 value만 지우고 key는 그대로 두는 것이다.

var obj = {
    bar: 1,
    foo: 2,
    baz: 3
};
obj.bar = undefined;
obj.foo = null;
delete obj.baz;

for(var i in obj) {
    if (obj.hasOwnProperty(i)) {
        console.log(i, '' + obj[i]);
    }
}

위 코드의 출력 결과는 baz만 제거했기 때문에 bar undefinedfoo null은 출력되고 baz와 관련된 것은 출력되지 않는다.

Notation of Keys

var test = {
    'case': 'I am a keyword, so I must be notated as a string',
    delete: 'I am a keyword, so me too' // SyntaxError가 난다.
};

프로퍼티는 따옴표 없는 문자열(plain characters)과 따옴표로 감싼 문자열(strings)을 모두 Key 값으로 사용할 수 있다. 하지만 위와 같은 코드는 JavaScript 파서의 잘못된 설계 때문에 구버전(ECMAScript 5 이전 버전)에서는 SystaxError가 발생할 것이다.

위 코드에서 문제가 되는 delete 키워드를 따옴표로 감싸면 구버전의 JavaScript 엔진에서도 제대로 해석될 것이다.

Prototype

Javascript는 클래스 스타일의 상속 모델을 사용하지 않고 프로토타입 스타일의 상속 모델을 사용한다.

'이 점이 JavaScript의 약점이다.'라고 말하는 사람들도 있지만 실제로는 prototypal inheritance 모델이 훨씬 더 강력하다. 그 이유는 프로토타입 모델에서 클래스 모델을 흉내 내기는 매우 쉽지만, 반대로 클래스 모델에서 프로토타입 모델을 흉내 내기란 매우 어렵기 때문이다.

실제로 Prototypal Inheritance 모델을 채용한 언어 중에서 JavaScript만큼 널리 사용된 언어가 없었기 때문에 두 모델의 차이점이 다소 늦게 정리된 감이 있다.

먼저 가장 큰 차이점은 프로토타입 체인이라는 것을 이용해 상속을 구현한다는 점이다.

function Foo() {
    this.value = 42;
}
Foo.prototype = {
    method: function() {}
};

function Bar() {}

// Foo의 인스턴스를 만들어 Bar의 prototype에 할당한다.
Bar.prototype = new Foo();
Bar.prototype.foo = 'Hello World';

// Bar 함수를 생성자로 만들고
Bar.prototype.constructor = Bar;

var test = new Bar() // bar 인스턴스를 만든다.

// 결과적으로 만들어진 프로토타입 체인은 다음과 같다. 
test [instance of Bar]
    Bar.prototype [instance of Foo] 
        { foo: 'Hello World' }
        Foo.prototype
            { method: ... }
            Object.prototype
                { toString: ... /* etc. */ }

위 코드에서 test 객체는 Bar.prototypeFoo.prototype을 둘 다 상속받았기 때문에 Foo에 정의한 method 함수에 접근할 수 있다. 그리고 프로토타입 체인에 있는 Foo 인스턴스의 value 프로퍼티도 사용할 수 있다. new Bar()를 해도 Foo 인스턴스는 새로 만들어지지 않고 Bar의 prototype에 있는 것을 재사용한다. 그래서 모든 Bar 인스턴스는 같은 value 프로퍼티를 공유한다.

프로토타입 탐색

객체의 프로퍼티에 접근하려고 하면 JavaScript는 해당 이름의 프로퍼티를 찾을 때까지 프로토타입 체인을 거슬러 올라가면서 탐색하게 된다.

프로토타입 체인을 끝까지 탐색했음에도(보통은 Object.prototype임) 불구하고 원하는 프로퍼티를 찾지 못하면 undefined를 반환한다.

prototype 프로퍼티

prototype 프로퍼티는 프로토타입 체인을 만드는 데 사용하고 어떤 값이든 할당할 수 있지만, primitive 값을 할당되면 무시한다.

function Foo() {}
Foo.prototype = 1; // 무시됨

반면에 위 예제처럼 객체를 할당하면 프로토타입 체인이 동적으로 잘 만들어진다.

성능

프로토타입 체인을 탐색하는 시간이 오래걸릴수록 성능에 부정적인 영향을 줄수있다. 특히 성능이 중요한 코드에서 프로퍼티 탐색시간은 치명적인 문제가 될수있다. 가령, 없는 프로퍼티에 접근하려고 하면 항상 프로토타입 체인 전체를 탐색하게 된다.

뿐만아니라 객체를 순회(Iterate)할때도 프로토타입 체인에 있는 모든 프로퍼티를 탐색하게 된다.

네이티브 프로토타입의 확장

종종 Object.prototype을 이용해 내장 객체를 확장하는 경우가 있는데, 이것도 역시 잘못 설계된 것중에 하나다.

위와 같이 확장하는 것을 Monkey Patching라고 부르는데 캡슐화를 망친다. 물론 Prototype같은 유명한 프레임워크들도 이런 확장을 사용하지만, 기본 타입에 표준도 아닌 기능들을 너저분하게 추가하는 이유를 여전히 설명하지 못하고 있다.

기본 타입을 확장해야하는 유일한 이유는 Array.forEach같이 새로운 JavaScript 엔진에 추가된 기능을 대비해 미리 만들어 놓는 경우 말고는 없다.

결론

프로토타입을 이용해 복잡한 코드를 작성하기 전에 반드시 프로토타입 상속 (Prototypal Inheritance) 모델을 완벽하게 이해하고 있어야 한다. 뿐만아니라 프로토타입 체인과 관련된 성능 문제로 고생하지 않으려면 프로토타입 체인이 너무 길지 않도록 항상 주의하고 적당히 끊어줘야 한다. 마지막으로 새로운 JavaScript 기능에 대한 호환성 유지 목적이 아니라면 절대로 네이티브 프로토타입을 확장하지마라.

hasOwnProperty

어떤 객체의 프로퍼티가 자기 자신의 프로퍼티인지 아니면 프로토타입 체인에 있는 것인지 확인하려면 hasOwnProperty 메소드를 사용한다. 그리고 이 메소드는 Object.prototype으로 부터 상속받아 모든 객체가 가지고 있다.

hasOwnProperty메소드는 프로토타입 체인을 탐색하지 않고, 프로퍼티를 다룰수있는 유일한 방법이다.

// Object.prototype을 오염시킨다.
Object.prototype.bar = 1; 
var foo = {goo: undefined};

foo.bar; // 1
'bar' in foo; // true

foo.hasOwnProperty('bar'); // false
foo.hasOwnProperty('goo'); // true

hasOwnProperty 메소드는 어떤 프로퍼티가 자기 자신의 프로퍼티인지 아닌지 정확하게 알려주기 때문에 객체의 프로퍼티를 순회할때 꼭 필요하다. 그리고 프로토타입 체인 어딘가에 정의된 프로퍼티만을 제외하는 방법은 없다.

hasOwnProperty 메소드도 프로퍼티다

JavaScript는 hasOwnProperty라는 이름으로 프로퍼티를 덮어 쓸수도 있다. 그래서 객체 안에 같은 이름으로 정의된 hasOwnProperty가 있을 경우, 본래 hasOwnProperty의 값을 정확하게 얻고 싶다면 다른 객체의 hasOwnProperty 메소드를 빌려써야 한다.

var foo = {
    hasOwnProperty: function() {
        return false;
    },
    bar: 'Here be dragons'
};

foo.hasOwnProperty('bar'); // 항상 false를 반환한다.

// 다른 객체의 hasOwnProperty를 사용하여 foo 객체의 프로퍼티 유무를 확인한다.
({}).hasOwnProperty.call(foo, 'bar'); // true

// Object에 있는 hasOwnProperty를 사용해도 된다.
Object.prototype.hasOwnProperty.call(obj, 'bar'); // true

결론

어떤 객체에 원하는 프로퍼티가 있는지 확인하는 가장 확실한 방법은 hasOwnProperty를 사용하는 것이다. for in loop에서 네이티브 객체에서 확장된 프로퍼티를 제외하고 순회하려면 hasOwnProperty와 함께 사용하길 권한다.

for in Loop

객체의 프로퍼티를 탐색할때 in 연산자와 마찬가지로 for in 문도 프로토타입 체인까지 탐색한다.

// Object.prototype을 오염시킨다.
Object.prototype.bar = 1;

var foo = {moo: 2};
for(var i in foo) {
    console.log(i); // bar와 moo 둘 다 출력한다.
}

for in문에 정의된 기본 동작을 바꿀순 없기 때문에 루프 안에서 불필요한 프로퍼티를 필터링 해야한다. 그래서 Object.prototypehasOwnProperty메소드를 이용해 본래 객체의 프로퍼티만 골라낸다.

hasOwnProperty로 필터링 하기

// 위의 예제에 이어서 
for(var i in foo) {
    if (foo.hasOwnProperty(i)) {
        console.log(i);
    }
}

위와 같이 사용해야 올바른 사용법이다. hasOwnProperty 때문에 오직 moo만 출력된다. hasOwnProperty가 없으면 이 코드는 Object.prototype으로 네이티브 객체가 확장될 때 에러가 발생할 수 있다.

따라서 Proptotype 라이브러리처럼 네이티브 객체를 프로토타입으로 확장한 프레임워크를 사용할 경우 for in 문에 hasOwnProperty를 사용하지 않을 경우 문제가 발생할 수 있다.

결론

hasOwnProperty를 항상 사용하길 권한다. 실제 코드가 동작하는 환경에서는 절대로 네이티브 객체가 프로토타입으로 확장됐다 혹은 확장되지 않았다를 가정하면 안된다.

함수

함수 선언과 함수 표현식

JavaScript에서 함수는 First Class Object다. 즉, 함수 자체가 또 다른 함수의 인자될 수 있다는 말이다. 그래서 익명 함수를 비동기 함수의 콜백으로 넘기는 것도 이런 특징을 이용한 일반적인 사용법이다.

함수 선언

function foo() {}

위와 같이 선언한 함수는 프로그램이 실행하기 전에 먼저 호이스트(Hoist) (스코프가 생성)되기 때문에 정의된 스코프(Scope) 안에서는 어디서든 이 함수를 사용할 수 있다. 심지어 함수를 정의하기 전에 호출해도 된다.

foo(); // 이 코드가 실행되기 전에 foo가 만들어지므로 잘 동작한다.
function foo() {}

함수 표현식

var foo = function() {};

위 예제는 foo 변수에 익명 함수를 할당한다.

foo; // 'undefined'
foo(); // TypeError가 난다.
var foo = function() {};

'var'문을 이용해 선언하는 경우, 코드가 실행되기 전에 'foo' 라는 이름의 변수를 스코프의 맨 위로 올리게 된다.(호이스트 된다) 이때 foo 값은 undefiend로 정의된다.

하지만 변수에 값을 할당하는 일은 런타임 상황에서 이루어지게 되므로 실제 코드가 실행되는 순간의 foo변수는 기본 값인 undefined이 된다.

이름있는 함수 표현식

이름있는 함수를 할당할때도 특이한 경우가 있다.

var foo = function bar() {
    bar(); // 이 경우는 동작 하지만, 
}
bar(); // 이 경우는 참조에러를 발생시킨다. 

foo 함수 스코프 밖에서는 foo 변수 외에는 다른 값이 없기 때문에 bar는 함수 밖에서 사용할 수 없지만 함수 안에서는 사용할 수 있다. 이와 같은 방법으로 자바스크립트에서 어떤 함수의 이름은 항상 그 함수의 지역 스코프 안에서 사용할수있다.

this의 동작 원리

다른 프로그래밍 언어에서 this가 가리키는 것과 JavaScript에서 this가 가리키는 것과는 좀 다르다. this가 가리킬 수 있는 객체는 정확히 5종류나 된다.

Global Scope에서

this;

Global Scope에서도 this가 사용될 수 있고 이때에는 Global 객체를 가리킨다.

함수를 호출할 때

foo();

이때에도 thisGlobal 객체를 가리킨다.

메소드로 호출할 때

test.foo(); 

이 경우에는 thistest를 가리킨다.

생성자를 호출할 때

new foo(); 

new 키워드로 생성자를 실행시키는 경우에 이 생성자 안에서 this는 새로 만들어진 객체를 가리킨다.

this가 가리키는 객체 정해주기.

function foo(a, b, c) {}

var bar = {};
foo.apply(bar, [1, 2, 3]); // a = 1, b = 2, c = 3으로 넘어간다.
foo.call(bar, 1, 2, 3); // 이것도... 

Function.prototypecall이나 apply 메소드를 호출하면 this가 무엇을 가리킬지 정해줄 수 있다. 호출할 때 첫 번째 인자로 this가 가리켜야 할 객체를 넘겨준다.

그래서 foo Function 안에서 this는 위에서 설명했던 객체 중 하나를 가리키는 것이 아니라 bar를 가리킨다.

대표적인 함정

this가 Global 객체를 가리키는 것도 잘못 설계된 부분 중 하나다. 괜찮아 보이지만 실제로는 전혀 사용하지 않는다.

Foo.method = function() {
    function test() {
        // 여기에서 this는 Global 객체를 가리킨다.
    }
    test();
}

test 에서 thisFoo를 가리킬 것으로 생각할 테지만 틀렸다. 실제로는 그렇지 않다.

test에서 Foo에 접근하려면 method에 Local 변수를 하나 만들고 Foo를 가리키게 하여야 한다.

Foo.method = function() {
    var that = this;
    function test() {
        // 여기에서 this 대신에 that을 사용하여 Foo에 접근한다.
    }
    test();
}

that은 this에 접근하기 위해 만든 변수다. closures와 함께 this의 값을 넘기는 데 사용할 수 있다.

Method할당 하기

JavaScript의 또다른 함정은 바로 함수의 별칭을 만들수 없다는 점이다. 별칭을 만들기 위해 메소드를 변수에 넣으면 자바스크립트는 별칭을 만들지 않고 바로 할당해 버린다.

var test = someObject.methodTest;
test();

첫번째 코드로 인해 이제 test는 다른 함수와 똑같이 동작한다. 그래서 test 함수 내부의 this도 더이상 someObject를 가리키지 않는다. (역주: test가 methodTest의 별칭이라면 methodTest 함수 내부의 this도 someObject를 똑같이 가리켜야 하지만 test의 this는 더이상 someObject가 아니다.)

이렇게 this를 늦게 바인딩해서 나타나는 약점때문에 늦은 바인딩이 나쁜 거라고 생각할수도 있지만, 사실 이런 특징으로 인해 프로토타입 상속(prototypal inheritance)도 가능해진다.

function Foo() {}
Foo.prototype.method = function() {};

function Bar() {}
Bar.prototype = Foo.prototype;

new Bar().method();

Bar 인스턴스에서 method를 호출하면 method에서 this는 바로 그 인스턴스를 가리킨다.

클로져(Closure)와 참조(Reference)

클로져는 JavaScript의 특장점 중 하나다. 클로저를 만들면 클로저 스코프 안에서 클로저를 만든 외부 스코프(Scope)에 항상 접근할 있다. JavaScript에서 스코프는 함수 스코프밖에 없기 때문에 기본적으로 모든 함수는 클로저가 될수있다.

private 변수 만들기

function Counter(start) {
    var count = start;
    return {
        increment: function() {
            count++;
        },

        get: function() {
            return count;
        }
    }
}

var foo = Counter(4);
foo.increment();
foo.get(); // 5

여기서 Counterincrement 클로저와 get 클로저 두 개를 반환한다. 이 두 클로저는 Counter 함수 스코프에 대한 참조를 유지하고 있기 때문에 이 함수 스코프에 있는 count 변수에 계속 접근할 수 있다.

Private 변수의 동작 원리

JavaScript에서는 스코프(Scope)를 어딘가에 할당해두거나 참조할수 없기 때문에 스코프 밖에서는 count 변수에 직접 접근할 수 없다. 접근할수 있는 유일한 방법은 스코프 안에 정의한 두 클로저를 이용하는 방법밖에 없다.

var foo = new Counter(4);
foo.hack = function() {
    count = 1337;
};

위 코드에서 foo.hack 함수는 Counter 함수 안에서 정의되지 않았기 때문에 이 함수가 실행되더라도 Counter 함수 스코프 안에 있는 count 값은 변하지 않는다. 대신 foo.hack 함수의 countGlobal 스코프에 생성되거나 이미 만들어진 변수를 덮어쓴다.

반복문에서 클로저 사용하기

사람들이 반복문에서 클로저를 사용할 때 자주 실수를 하는 부분이 있는데 바로 인덱스 변수를 복사할때 발생한다.

for(var i = 0; i < 10; i++) {
    setTimeout(function() {
        console.log(i);  
    }, 1000);
}

이 코드는 0부터 9까지의 수를 출력하지 않고 10만 열 번 출력한다.

타이머에 설정된 익명 함수는 변수 i에 대한 참조를 들고 있다가 console.log가 호출되는 시점에 i의 값을 사용한다. console.log가 호출되는 시점에서 for loop는 이미 끝난 상태기 때문에 i 값은 10이 된다.

기대한 결과를 얻으려면 i 값을 복사해 두어야 한다.

앞의 참조 문제 해결하기

반복문의 index 값을 복사하는 가장 좋은 방법은 익명함수로 랩핑Anonymous Wrapper하는 방법이다.

for(var i = 0; i < 10; i++) {
    (function(e) {
        setTimeout(function() {
            console.log(e);  
        }, 1000);
    })(i);
}

이 익명 함수에 i를 인자로 넘기면 이 함수의 파라미터 e에 i의 이 복사되어 넘어갈 것이다.

그리고 setTimeout는 익명 함수의 파라미터인 e에 대한 참조를 갖게 되고 e값은 복사되어 넘어왔으므로 loop의 상태에 따라 변하지 않는다.

또다른 방법으로 랩핑한 익명 함수에서 출력 함수를 반환하는 방법도 있다. 아래 코드는 위 코드와 동일하게 동작한다.

for(var i = 0; i < 10; i++) {
    setTimeout((function(e) {
        return function() {
            console.log(e);
        }
    })(i), 1000)
}

arguments 객체

JavaScript의 모든 함수 스코프에는 arguments라는 특별한 변수가 있다. 이 변수는 함수에 넘겨진 모든 인자에 대한 정보가 담겨 있다.

arguments 객체는 Array가 아니다. 물론 length 프로퍼티도 있고 여러모로 Array와 비슷하게 생겼지만 Array.prototype을 상속받지는 않았다.

그래서 arguments에는 push, pop, slice 같은 표준 메소드가 없다. 일반 for문을 이용해 순회는 할수 있지만, Array의 메소드를 이용하려면 arguments를 Array로 변환해야 한다.

Array로 변환하기

다음 코드는 arguments에 있는 객체를 새로운 Array에 담아 반환한다.

Array.prototype.slice.call(arguments);

이 변환 과정은 느리기 때문에 성능이 중요한 부분에 사용하는 것은 별로 바람직하지 못 하다.

arguemnts 객체 넘기기

어떤 함수에서 다른 함수로 arguments 객체를 넘길 때에는 다음과 같은 방법을 권한다. (역주: foo 함수는 bar 함수 한번 랩핑한 함수다. )

function foo() {
    bar.apply(null, arguments);
}
function bar(a, b, c) {
    // 내곡동에 땅이라도 산다.
}

또 다른 방법으로는 함수를 랩핑하지 않고, 풀어서 callapply를 함께 사용하는 방법이 있다. (역주: 프로토타입에 있는 method를 호출하기 전에 Foo 객체 안에 있는 method로 한번더 필터링하는 효과가 있다. )

function Foo() {}

Foo.prototype.method = function(a, b, c) {
    console.log(this, a, b, c);
};

// "method"를 풀어 쓴(unbound) 버전
// 이 Function의 인자: this, arg1, arg2...argN
Foo.method = function() {

    // 결과: Foo.prototype.method.call(this, arg1, arg2... argN)
    Function.call.apply(Foo.prototype.method, arguments);
};

일반 파라미터와 arguments 객체의 인덱스

일반 파라미터와 arguments 객체의 프로퍼티는 모두 gettersetter를 가진다.

그래서 파라미터나 arguments 객체의 프로퍼티의 값을 바꾸면 둘 다 바뀐다.

function foo(a, b, c) {
    arguments[0] = 2;
    a; // 2

    b = 4;
    arguments[1]; // 4

    var d = c;
    d = 9;
    c; // 3
}
foo(1, 2, 3);

성능에 대한 오해와 진실.

arguments 객체는 항상 만들어지지만 두가지 예외사항이 있다. arguments라는 이름으로 변수를 함수 안에 정의하거나 arguments 객체로 넘겨받는 인자중 하나라도 정식 인자로 받아서 사용하면 arguemnts 객체는 만들어지지 않는다. 하지만 뭐 이런 경우들은 어차피 arguments 객체를 안쓰겠다는 의미니까 상관 없다.

그리고 gettersetter는 항상 생성되기 때문에 getter/setter를 사용하는 것은 성능에 별 영향을 끼치지 않는다. 예제처럼 단순한 코드가 아니라 arguments 객체를 다방면으로 활용하는 실제 코드에서도 마찬가지다.

그러나 예외도 있다. 최신 JavaScript 엔진에서 arguments.callee를 사용하면 성능이 확 떨어진다.

function foo() {
    arguments.callee; // 이 함수를 가리킨다.
    arguments.callee.caller; // 이 함수를 호출한 부모함수를 가리킨다.
}

function bigLoop() {
    for(var i = 0; i < 100000; i++) {
        foo(); // 원래 인라인 돼야 하는디...
    }
}

위 코드에서 'foo' 함수는 자기 자신과 자신을 호출한 함수를 알아야 하기 때문에 더이상 인라인되지 않는다. 이렇게 쓰면 인라인이 주는 성능상 장점을 포기해야 하는데다가 이 함수가 호출되는 상황(calling context)에 의존하게 돼 버려서 캡슐화(Encapsulation)도 해친다. (역주: 보통 코드가 컴파일 될때 코드를 인라인 시키면서 최적화 하는데, 위와 같이 arguments.callee나 caller를 사용하게 되면 런타임시에 해당 함수가 결정되므로 인라인 최적화를 할수가 없다.)

arguments.callee와 arguments.callee의 프로퍼티들은 절대 사용하지 말자!.

생성자

JavaScript의 생성자는 다른 언어들과 다르게 new 키워드로 호출되는 함수가 생성자가 된다.

생성자로 호출된 함수의 this 객체는 새로 생성된 객체를 가리키고, 새로 만든 객체의 prototype에는 생성자의 prototype이 할당된다.

그리고 생성자에 명시적인 return 구문이 없으면 this가 가리키는 객체를 반환한다.

function Foo() {
    this.bla = 1;
}

Foo.prototype.test = function() {
    console.log(this.bla);
};

var test = new Foo();

위 코드는 new 키워드가 실행되는 시점에 Foo를 생성자로 호출하고 Foo.prototype을 새 객체의 prototype에 할당한다.

아래 코드와 같이 생성자에 명시적인 return 문이 있는 경우에는 반환하는 값이 객체인 경우에만 그 값을 반환한다.

function Bar() {
    return 2;
}
new Bar(); // 새 객체를 만들어 반환

function Test() {
    this.value = 2;

    return {
        foo: 1
    };
}
new Test(); // 명시한 객체를 반환

new 키워드가 없으면 그 함수는 객체를 반환하지 않는다.

function Foo() {
    this.bla = 1; // 전역객체에 할당된다.
}
Foo(); // undefined

위 예제는 그때그때 다르게 동작한다. 그리고 this 객체의 동작 원리에 따라서 Foo 함수안의 this의 값은 Global 객체를 가리키게된다. (역주: 결국 new 키워드를 빼고, 코드를 작성할 경우 원치 않은 this 참조 오류가 발생할 수 있다.)

팩토리

생성자가 객체를 반환하면 new 키워드를 생략할 수 있다.

function Bar() {
    var value = 1;
    return {
        method: function() {
            return value;
        }
    }
}
Bar.prototype = {
    foo: function() {}
};

new Bar();
Bar();

new 키워드의 유무과 관계없이 Bar 생성자의 동작은 동일한다. 즉 클로저가 할당된 method 프로퍼티가 있는 새로운 객체를 만들어 반환한다.

new Bar()로 호출되는 생성자는 반환되는 객체의 prototype 프로퍼티에 아무런 영향을 주지 않는다. 객체를 반환하지 않는 생성자로 만들어지는 경우에만 객체의 prototype이 생성자의 것으로 할당된다.

그러니까 이 예제에서 new 키워드의 유무는 아무런 차이가 없다. (역주: 생성자에 객체를 만들어 명시적으로 반환하면 new 키워드에 관계없이 잘 동작하는 생성자를 만들수있다. 즉, new 키워드가 빠졌을때 발생하는 this 참조 오류를 방어해준다.)

팩토리로 객체 만들기

new 키워드를 빼먹었을 때 버그가 생긴다는 이유로 아예 new를 사용하지 말 것을 권하기도 한다.

객체를 만들고 반환해주는 팩토리를 사용하여 new 키워드 문제를 회피할 수 있다.

function Foo() {
    var obj = {};
    obj.value = 'blub';

    var private = 2;
    obj.someMethod = function(value) {
        this.value = value;
    }

    obj.getPrivate = function() {
        return private;
    }
    return obj;
}

new 키워드가 없어도 잘 동작하고 private 변수를 사용하기도 쉽다. 그렇지만, 단점도 있다.

  1. prototype으로 메소드를 공유하지 않으므로 메모리를 좀 더 사용한다.
  2. 팩토리를 상속하려면 모든 메소드를 복사하거나 객체의 prototype에 객체를 할당해 주어야 한다.
  3. new 키워드를 누락시켜서 prototype chain을 끊어버리는 것은 아무래도 언어의 의도에 어긋난다.

결론

new 키워드가 생략되면 버그가 생길 수 있지만 그렇다고 prototype을 사용하지 않을 이유가 되지 않는다. 애플리케이션에 맞는 방법을 선택하는 것이 나을 거고 어떤 방법이든 *엄격하고 한결같이 지켜야 한다.

스코프와 네임스페이스

JavaScript는 '{}' Block이 배배 꼬여 있어도 문법적으로는 잘 처리하지만, Block Scope은 지원하지 않는다. 그래서 JavaScript에서는 항상 함수 스코프를 사용한다.

function test() { // Scope
    for(var i = 0; i < 10; i++) { // Scope이 아님
        // count
    }
    console.log(i); // 10
}

그리고 JavaScript에는 Namepspace 개념이 없기 때문에 모든 값이 하나의 전역 스코프에 정의된다.

변수를 참조 할 때마다 JavaScript는 해당 변수를 찾을 때까지 상위 방향으로 스코프를 탐색한다. 변수 탐색하다가 전역 스코프에서도 찾지 못하면 ReferenceError를 발생시킨다.

전역 변수 문제.

// script A
foo = '42';

// script B
var foo = '42'

이 두 스크립트는 전혀 다르다. Script A는 전역 스코프에 foo라는 변수를 정의하는 것이고 Script B는 스코프에 변수 foo를 정의하는 것이다.

다시 말하지만, 이 둘은 전혀 다르고 var가 없을 때 특별한 의미가 있다.

// Global Scope
var foo = 42;
function test() {
    // local Scope
    foo = 21;
}
test();
foo; // 21

test 함수 안에 있는 'foo' 변수에 var 구문을 빼버리면 Global Scope의 foo의 값을 바꿔버린다. '뭐 이게 뭐가 문제야'라고 생각될 수 있지만 수천 줄인 JavaScript 코드에서 var를 빼먹어서 생긴 버그를 해결하는 것은 정말 어렵다.

// Global Scope
var items = [/* some list */];
for(var i = 0; i < 10; i++) {
    subLoop();
}

function subLoop() {
    // Scope of subLoop
    for(i = 0; i < 10; i++) { // var가 없다.
        // 내가 for문도 해봐서 아는데...
    }
}

subLoop 함수는 전역 변수 i의 값을 변경해버리기 때문에 외부에 있는 for문은 subLoop을 한번 호출하고 나면 종료된다. 두 번째 for문에 var를 사용하여 i를 정의하면 이 문제는 생기지 않는다. 즉, 의도적으로 외부 스코프의 변수를 사용하는 것이 아니라면 var를 꼭 넣어야 한다.

지역 변수

JavaScript에서 지역 변수는 함수의 파라미터var로 정의한 변수밖에 없다.

// 전역 공간 var foo = 1; var bar = 2; var i = 2;

function test(i) {
    // test 함수의 지역 공간
    i = 5;

    var foo = 3;
    bar = 4;
}
test(10);

foo 변수와 i 변수는 test함수 스코프에 있는 지역 변수라서 전역 공간에 있는 foo, i 값은 바뀌지 않는다. 하지만 bar는 전역 변수이기 때문에 전역 공간에 있는 bar의 값이 변경된다.

호이스팅(Hoisting)

JavaScript는 선언문을 모두 호이스트(Hoist)한다. 호이스트란 var 구문이나 function 선언문을 해당 스코프의 맨 위로 옮기는 것을 말한다.

bar();
var bar = function() {};
var someValue = 42;

test();
function test(data) {
    if (false) {
        goo = 1;

    } else {
        var goo = 2;
    }
    for(var i = 0; i < 100; i++) {
        var e = data[i];
    }
}

코드를 본격적으로 실행하기 전에 JavaScript는 var 구문과 function 선언문을 해당 스코프의 맨위로 옮긴다.

// var 구문이 여기로 옮겨짐.
var bar, someValue; // default to 'undefined'

// function 선언문도 여기로 옮겨짐
function test(data) {
    var goo, i, e; // Block Scope은 없으므로 local 변수들은 여기로 옮겨짐
    if (false) {
        goo = 1;

    } else {
        goo = 2;
    }
    for(i = 0; i < 100; i++) {
        e = data[i];
    }
}

bar(); // bar()가 아직 'undefined'이기 때문에 TypeError가 남
someValue = 42; // Hoisting은 할당문은 옮기지 않는다.
bar = function() {};

test();

블록 스코프(Block Scope)는 없으므로 for문과 if문 안에 있는 var 구문들까지도 모두 함수 스코프 앞쪽으로 옮겨진다. 그래서 if Block의 결과는 좀 이상해진다.

원래 코드에서 if Block은 전역 변수 goo를 바꾸는 것처럼 보였지만 호이스팅(Hoisting) 후에는 지역 변수를 바꾼다.

호이스팅을 모르면 다음과 같은 코드는 ReferenceError를 낼 것으로 생각할 것이다.

// SomeImportantThing이 초기화됐는지 검사한다.
if (!SomeImportantThing) {
    var SomeImportantThing = {};
}

var 구문은 전역 스코프의 맨위로 옮겨지기 때문에 이 코드는 잘 동작한다.

var SomeImportantThing;

// SomeImportantThing을 여기서 초기화하거나 말거나...

// SomeImportantThing는 선언돼 있다.
if (!SomeImportantThing) {
    SomeImportantThing = {};
}

이름 찾는 순서

JavaScript의 모든 Scope은 현 객체를 가리키는 this를 가지고 있다. 전역 스코프에도 this가 있다.

함수 스코프에는 arguments라는 변수가 하나 더 있다. 이 변수는 함수에 인자로 넘겨진 값들이 담겨 있다.

예를 들어 함수 스코프에서 foo라는 변수에 접근할 때 JavaScript는 다음과 같은 순서로 찾는다.

  1. 해당 Scope에서 var foo 구문으로 선언된 것을 찾는다.
  2. Function 파라미터에서 foo라는 것을 찾는다.
  3. 해당 Function 이름이 foo인지 찾는다.
  4. 상위 Scope으로 있는지 확인하고 있으면 #1부터 다시 한다.

네임스페이스

JavaScript에서는 전역 공간(Namepspace) 하나밖에 없어서 변수 이름이 중복되기 쉽다. 하지만 이름없는 랩퍼(Anonymous Wrappers)를 통해 쉽게 피해갈 수 있다.

(function() {
    // 일종의 네임스페이스라고 할 수 있다.

    window.foo = function() {
        // 이 클로저는 전역 스코프에 노출된다.
    };

})(); // 함수를 정의하자마자 실행한다.

이름없는 함수는 표현식(expressions)이기 때문에 호출되려면 먼저 평가(Evaluate)돼야 한다.

( // 소괄호 안에 있는 것을 먼저 평가한다.
function() {}
) // 그리고 함수 객체를 반환한다.
() // 평가된 결과를 호출한다.

함수를 평가하고 바로 호출하는 방법이 몇가지 더 있다. 문법은 다르지만 똑같다.

// 함수를 평가하자마자 호출하는 방법들...
!function(){}();
+function(){}();
(function(){}());
// 등등...

결론

코드를 캡슐화할 때는 항상 이름없는 랩퍼(Anonymous Wrapper)로 네임스페이스를 만들어 사용할 것을 추천한다. 이 래퍼(Wrapper)는 이름이 중복되는 것을 막아 주고 더 쉽게 모듈화할 수 있도록 해준다.

그리고 전역 변수를 사용하는 것은 좋지 못한 습관이다. 이유야 어쨌든 에러 나기 쉽고 관리하기도 어렵다.

Array

배열 순회와 프로퍼티

JavaScript에서는 배열(Array)도 객체(Object)지만 객체 순회(Iterate)를 할 때 for in을 사용해서 좋을 게 없다. 실제로 배열을 탐색할때 for in문 사용하지 말아야 할 이유가 매우 많다.

for in은 프로토타입 체인에 있는 프로퍼티를 모두 훑는(enumerate) 데다가 객체 자신의 프로퍼티만 훑으려면 hasOwnProperty를 사용해야 하기 때문에 for보다 20배 느리다.

배열 순회

배열을 순회 할때는 일반적인 for문을 사용하는 것이 가장 빠르다.

var list = [1, 2, 3, 4, 5, ...... 100000000];
for(var i = 0, l = list.length; i < l; i++) {
    console.log(list[i]);
}

이 예제에서 l = list.length로 배열의 length 값을 캐시해야 한다는 것을 꼭 기억해야 한다.

매번 반복할때마다 배열에 있는 length 프로퍼티에 접근하는 것은 좀 부담스럽다. 최신 JavaScript 엔진은 이 일을 알아서 처리해주기도 하지만 코드가 늘 새 엔진에서 실행되도록 보장할 방법이 없다.

실제로 캐시 하지 않으면 성능이 반으로 줄어든다.

length 프로퍼티

length 프로퍼티의 getter는 단순히 Array 안에 있는 엘리먼트의 개수를 반환하고 setter는 배열을 할당한 수만큼 잘라 버린다.

var foo = [1, 2, 3, 4, 5, 6];
foo.length = 3;
foo; // [1, 2, 3]

foo.length = 6;
foo; // [1, 2, 3]

현재 크기보다 더 작은 값을 할당하면 배열을 자르지만, 현재 크기보다 더 큰 값을 할당한다고 해서 배열을 늘리진 않는다.

결론

최적의 성능을 위해서는 for문을 사용하고 length 프로퍼티 값을 캐시해야 한다. 배열에 for in을 사용하면 성능도 떨어지고 버그 나기도 쉽다.

배열 생성자

배열을 만들때 배열 생성자에 파라미터를 넣어 만드는 방법은 헷갈릴수있다. 그래서 항상 각 괄호([]) 노테이션을 이용해 배열을 만들 것을 권한다

[1, 2, 3]; // Result: [1, 2, 3]
new Array(1, 2, 3); // Result: [1, 2, 3]

[3]; // Result: [3]
new Array(3); // Result: []
new Array('3') // Result: ['3']

배열 생성자에 숫자를 인자로 넣으면 그 숫자 크기 만큼의 빈 배열을 반환한다. 즉 배열의 length는 그 숫자가 된다. 이때 생성자는 단지 length 프로퍼티에 그 숫자를 할당하기만 하고 배열은 실제로 초기화 하지도 않는다.

var arr = new Array(3);
arr[1]; // undefined
1 in arr; // false, 이 인덱스는 초기화되지 않음.

for문을 사용하지 않고 문자열을 더하는 경우에는 length 프로퍼티에 숫자를 할당해주는 기능이 유용할 때도 있다.

new Array(count + 1).join(stringToRepeat);

결론

배열 생성자는 가능하면 사용하지 말고, 각 괄호 ([]) 노테이션이을 사용하자. 후자가 더 간략하고 명확할 뿐만 아니라 보기도 좋다.

타입

객체 비교하기

JavaScript에서 객체를 비교하는 방법은 두 가지가 있다.

이중 등호 연산자

이중 등호 연산자는 ==을 말한다.

JavaScript는 Weak Typing을 따르기 때문에 이중 등호를 이용해 비교할 때 두 객체의 자료형을 강제로 변환한다.

""           ==   "0"           // false
0            ==   ""            // true
0            ==   "0"           // true
false        ==   "false"       // false
false        ==   "0"           // true
false        ==   undefined     // false
false        ==   null          // false
null         ==   undefined     // true
" \t\r\n"    ==   0             // true

이 표는 이중 등호를 사용하면 왜 안되는지를 보여준다. 이 복잡한 변환 규칙은 실제로 골치 아픈 버그를 만들어 낸다.

게다가 강제로 타입을 변환하게 되면 성능에도 영향을 준다. 예를 들어 문자와 숫자를 비교하려면 반드시 먼저 문자를 숫자로 변환해야 한다.

삼중 등호 연산자

삼중 등호 연산자는 ===을 말한다.

삼중 등호는 강제로 타입을 변환하지 않는다는 사실을 제외하면 이중 등호와 동일하다.

""           ===   "0"           // false
0            ===   ""            // false
0            ===   "0"           // false
false        ===   "false"       // false
false        ===   "0"           // false
false        ===   undefined     // false
false        ===   null          // false
null         ===   undefined     // false
" \t\r\n"    ===   0             // false

위 결과가 훨씬 더 명확하고 문제가 쉽게 드러난다. 삼중 등호를 사용하면 코드를 좀 더 튼튼하게 만들수 있고, 비교하는 두 객체의 타입이 다르면 더 좋은 성능을 얻을 수도 있다.

객체 비교하기

이중 등호와(==)와 삼중 등호(===)는 둘 다 값을 비교하는 연산이지만 피연산자중에 Object 타입이 하나라도 있으면 다르게 동작한다.

{} === {};                   // false
new String('foo') === 'foo'; // false
new Number(10) === 10;       // false
var foo = {};
foo === foo;                 // true

두 연산자 모두 두 객체의 값이 같은지를 비교하지 않고, 두 객체가 같은 객체(identity)인지를 비교한다. C에서 포인터를 비교하거나 Python의 is처럼 같은 인스턴스인지 비교하는 것이다.

결론

삼중 등호 연산자를 사용할 것을 강력하게 권한다. 비교하기 위해서 타입 변환이 필요하면 언어의 복잡한 변환 규칙에 맡기지 말고 꼭 명시적으로 변환한 후에 비교해야 한다.

typeof 연산자

typeof 연산자도 instanceof 연산자와 함께 JavaScript에서 치명적으로 잘못 설계된 부분이다. 이건 정말이지 아무짝에도 쓸모가 없다.

instanceof 연산자는 그래도 여전히 쓸만한 데가 좀 있는데 typeof 연산자는 객체의 타입을 검사하는 것 외에는 쓸만한데가 없고, 이마저도 거의 쓸일이 없다.

JavaScript 타입 표

Value               Class      Type
-------------------------------------
"foo"               String     string
new String("foo")   String     object
1.2                 Number     number
new Number(1.2)     Number     object
true                Boolean    boolean
new Boolean(true)   Boolean    object
new Date()          Date       object
new Error()         Error      object
[1,2,3]             Array      object
new Array(1, 2, 3)  Array      object
new Function("")    Function   function
/abc/g              RegExp     object (function in Nitro/V8)
new RegExp("meow")  RegExp     object (function in Nitro/V8)
{}                  Object     object
new Object()        Object     object

위 표에서 Typetypeof가 반환하는 값이다. 위 표에서처럼 일치되는 값이 거의 없다.

위 표에서 Class는 객체 내부에 있는 [[Class]] 프로퍼티의 값을 말한다.

[[Class]] 프로퍼티의 값을 가져다 쓰려면 Object.prototypetoString 메소드를 사용한다.

객체의 클래스

표준에 의하면 [[Class]] 값을 얻는 방법은 Object.prototype.toString 하나뿐이다.

function is(type, obj) {
    var clas = Object.prototype.toString.call(obj).slice(8, -1);
    return obj !== undefined && obj !== null && clas === type;
}

is('String', 'test'); // true
is('String', new String('test')); // true

Object.prototype.toStringthis[[Class]] 값을 가져오는 것이니까 this를 obj로 바꾸어 사용한다.

변수가 Undefined인지 확인하기

typeof foo !== 'undefined'

위 코드는 foo가 정의됐는지 아닌지를 확인해준다. 정의되지 않은 변수에 접근하면 ReferenceError 나는데 이것을 방지할 수 있다. typeof가 유용한 건 이때뿐이다.

결론

객체의 타입을 검사하려면 Object.prototype.toString를 사용해야 한다. 다른 방법은 신뢰할 수 없다. 위 표에서 보여준 것처럼 typeof가 반환하는 값은 표준에 나와 있지 않기 때문에 구현방법도 제각각이다.

변수가 정의됐는지 확인할 때를 제외하고 가급적 typeof는 피해야한다.

instanceof 연산자

instanceof연산자는 두 피연산자의 생성자를 비교할때 사용하고 직접 만든 객체를 비교할 때 매우 유용하다. 내장 타입에 쓰는 경우에는 typeof처럼 거의 쓸모가 없다.

커스텀 객체를 intanceof로 비교하기

function Foo() {}
function Bar() {}
Bar.prototype = new Foo();

new Bar() instanceof Bar; // true
new Bar() instanceof Foo; // true

// Bar.prototype에 함수 객체인 Foo를 할당하면
// Bar의 인스턴스는 Foo의 인스턴스가 아니다.
Bar.prototype = Foo;
new Bar() instanceof Foo; // false

기본 내장 객체 타입을 intanceof로 비교하기

new String('foo') instanceof String; // true
new String('foo') instanceof Object; // true

'foo' instanceof String; // false
'foo' instanceof Object; // false

JavaScript 컨텍스트마다(웹 브라우저의 도큐먼트 같은) 객체의 생성자는 다를 수밖에 없어서 instanceof는 다른 JavaScript 컨텍스트에 있는(웹 브라우저의 다른 도큐먼트에 있는) 객체와는 비교할 수 없다.

결론

instanceof는 한 JavaScript 컨텍스트 내에서 사용자가 만든 타입의 객체를 비교할 때에만 유용하다. typeof처럼 다른 목적으로는 사용하지 않는 것이 좋다.

타입 캐스팅

JavaScript는 Weak Typing 언어이기 때문에 필요할 때마다 알아서 타입을 변환한다.

// 다음은 모두 true
new Number(10) == 10; // Number.toString()이 호출되고 
                      // 다시 Number로 변환된다.

10 == '10';           // 스트링은 Number로 변환된다.
10 == '+10 ';         // 이상한 스트링
10 == '010';          // 엉뚱한 스트링
isNaN(null) == false; // null은 NaN이 아녀서 0으로 변환된다.

// 다음은 모두 false
10 == 010;
10 == '-10';

위와 같은 문제들은 *반드시 삼중 등호 연산자를 이용해 해결하길 권한다. 물론 삼중 등호로 많은 결점을 보완할 수 있지만, 여전히 weak typing 시스템 때문에 생기는 많은 문제가 남아있다.

기본 타입 생성자

NumberString 같은 기본 타입들의 생성자는 new 키워드가 있을 때와 없을 때 다르게 동작한다.

new Number(10) === 10;     // False, Object와 Number
Number(10) === 10;         // True, Number와 Number
new Number(10) + 0 === 10; // True, 타입을 자동으로 변환해주기 때문에 

new 키워드와 함께 Number 같은 기본 타입의 생성자를 호출하면 객체를 생성하지만 new 없이 호출하면 형 변환만 시킨다.

그리고 객체가 아니라 단순히 값이나 리터럴을 사용하면 타입 변환이 더 많이 일어난다.

가능한 정확하게 타입을 변환해주는 것이 최선이다.

스트링으로 변환하기

'' + 10 === '10'; // true

숫자를 빈 스트링과 더하면 쉽게 스트링으로 변환할 수 있다.

숫자로 변환하기

+'10' === 10; // true

+ 연산자만 앞에 붙여주면 스트링을 쉽게 숫자로 변환할 수 있다.

Boolean으로 변환하기

'!' 연산자를 두 번 사용하면 쉽게 Boolean으로 변환할 수 있다.

!!'foo';   // true
!!'';      // false
!!'0';     // true
!!'1';     // true
!!'-1'     // true
!!{};      // true
!!true;    // true

핵심

eval을 사용하면 안 될까?

eval 함수는 JavaScript 문자열을 지역 스코프에서 실행한다.

var foo = 1;
function test() {
    var foo = 2;
    eval('foo = 3');
    return foo;
}
test(); // 3
foo; // 1

eval함수는 eval이라는 이름으로 직접 실행할 때에만 지역 스코프에서 실행된다. 그리고 eval이라는 이름에 걸맞게 악명또한 높다.

var foo = 1;
function test() {
    var foo = 2;
    var bar = eval;
    bar('foo = 3');
    return foo;
}
test(); // 2
foo; // 3

어쨌든 eval은 사용하지 말아야 한다. eval을 사용하는 99.9%는 사실 eval 없이도 만들수있다.

가짜 eval

setTimeoutsetInterval은 첫 번째 인자로 스트링을 입력받을 수 있다. 이 경우에는 eval을 직접 호출하는 것이 아니라서 항상 Global Scope에서 실행된다.

보안 이슈

eval은 어떤 코드라도 무조건 실행하기 때문에 보안 문제도 있다. 따라서 신뢰하지 못하거나 모르는 코드가 포함되어 있을 경우 절대로 사용해서는 안된다.

결론

eval은 사용하지 않는 게 좋다. eval을 사용하는 모든 코드는 성능, 보안, 버그 문제를 일으킬 수 있다. 만약 eval이 필요해지면 설계를 변경하여 eval이 필요 없게 만들어야 한다.

undefinednull

JavaScript는 nothing을 표현할때 nullundefined 두 가지로 표현할 수 있고 그중 undefined가 더 유용하다.

undefined도 변수

undefinedundefined라는 값을 가지는 데이터 형식이다.

undefined는 상수도 아니고 JavaScript의 키워드도 아니다. 그냥 undefined라는 이름의 Global 변수이고 이 변수에는 undefined라고 할당돼 있다. 그래서 이 Global 변수의 값을 쉽게 바꿀 수 있다.

undefined 값이 반환될 때:

  • global 변수 undefined에 접근할 때.
  • 선언은 했지만 아직 초기화하지 않은 변수에 접근할 때.
  • return 구문이 없는 함수는 암묵적으로 undefined를 반환함.
  • return 구문으로 아무것도 반환하지 않을 때.
  • 없는 프로퍼티를 찾을 때.
  • 함수 인자가 생략될 때.
  • undefined가 할당된 모든 것.
  • void(expression) 형식으로 된 표현

undefined가 바뀔 때를 대비하기

global 변수 undefinedundefined라는 객체를 가리키는 것뿐이기 때문에 새로운 값을 할당한다고 해도 undefined의 값 자체가 바뀌는 것이 아니다.

그래서 undefined와 비교하려면 먼저 undefined의 값을 찾아와야 한다.

undefined 변수가 바뀔 때를 대비해서 undefined라는 변수를 인자로 받는 anonymous wrapper로 감싸고 인자를 넘기지 않는 꼼수를 사용한다.

var undefined = 123;
(function(something, foo, undefined) {
    // Local Scope에 undefined를 만들어서
    // 원래 값을 가리키도록 했다.

})('Hello World', 42);

wrapper 안에 변수를 새로 정의하는 방법으로도 같은 효과를 볼 수 있다.

var undefined = 123;
(function(something, foo) {
    var undefined;
    ...

})('Hello World', 42);

이 두 방법의 차이는 minified했을 때 4바이트만큼 차이 난다는 것과 한쪽은 wrapper 안에 var 구문이 없다는 것밖에 없다.

Null 객체의 용도

JavaScript 언어에서는 undefined를 다른 언어의 null 처럼 쓴다. 진짜 null은 그냥 데이터 타입 중 하나일 뿐이지 더도덜도 아니다.

JavaScript를 깊숙히 건드리는 것이 아니면 null 대신 undefined를 사용해도 된다(Foo.prototype = null같이 프로토타입 체인을 끊을 때는 null을 사용한다).

자동으로 삽입되는 쎄미콜론

JavaScript는 C와 문법이 비슷하지만, 꼭 코드에 쎄미콜론을 사용하도록 강제하지는 않는다. 그래서 생략할 수 있다.

사실 JavaScript는 쎄미콜론이 꼭 있어야 하고 없으면 이해하지 못한다. 그래서 JavaScript 파서는 쎄미콜론이 없으면 자동으로 쎄미콜론을 추가한다.

var foo = function() {
} // 쎄미콜론이 없으니 에러 난다.
test()

파서는 쎄미콜론을 삽입하고 다시 시도한다.

var foo = function() {
}; // 에러가 없어짐.
test()

쎄미콜론을 자동으로 삽입한 것이 대표적인 JavaScript 설계 오류다. 쎄미콜론 유무에 따라 전혀 다른 코드가 될 수 있다.

어떻게 다를까?

코드에 쎄미콜론이 없으면 파서가 어디에 넣을지 결정한다.

(function(window, undefined) {
    function test(options) {
        log('testing!')

        (options.list || []).forEach(function(i) {

        })

        options.value.test(
            'long string to pass here',
            'and another long string to pass'
        )

        return
        {
            foo: function() {}
        }
    }
    window.test = test

})(window)

(function(window) {
    window.someLibrary = {}

})(window)

파서는 이 코드에 쎄미콜론을 다음과 같이 삽입한다.

(function(window, undefined) {
    function test(options) {

        // 쎄미콜론을 넣는 것이 아니라 줄을 합친다.
        log('testing!')(options.list || []).forEach(function(i) {

        }); // <- 여기

        options.value.test(
            'long string to pass here',
            'and another long string to pass'
        ); // <- 여기

        return; // <- 여기에 넣어서 그냥 반환시킨다.
        { // 파서는 단순 블럭이라고 생각하고

            // 단순한 레이블과 함수
            foo: function() {}
        }; // <- 여기
    }
    window.test = test; // <- 여기

// 이 줄도 합쳐진다.
})(window)(function(window) {
    window.someLibrary = {}; // <- 여기

})(window); //<- 여기에 파서는 쎄미콜론을 넣는다.

파서는 완전히 다른 코드로 만들어 버린다. 이것은 오류다.

괄호 해석

파서는 괄호에는 쎄미콜론을 넣지 않는다.

log('testing!')
(options.list || []).forEach(function(i) {})

그래서 다음과 같이 한줄로 코드를 바꾼다.

log('testing!')(options.list || []).forEach(function(i) {})

이렇게 한줄로 바뀌면 log 함수가 함수를 반환할 가능성이 거의 없으므로 undefined is not a function이라는 TypeError가 발생한다.

결론

쎄미콜론은 반드시 사용해야 한다. 그리고 {}도 생략하지 않고 꼭 사용하는 것이 좋다. 한 줄밖에 안 되는 if / else 블럭에서도 꼭 사용해야 한다. 이 두 가지 규칙을 잘 지키면 JavaScript 파서가 잘못 해석하는 일을 미리 방지하고 코드도 튼튼해진다.

delete 연산자

간단히 말해서 전역 변수와 전역 함수 그리고 DontDelete 속성을 가진 자바스크립트 객체는 삭제할 수 없다.

Global 코드와 Function 코드

전역이나 함수 스코프에 정의한 함수나 변수는 모두 Activation 객체나 전역 객체의 프로퍼티다. 이 프로퍼티는 모두 DontDelete 속성을 가진다. 전역이나 함수 코드에 정의한 변수와 함수는 항상 DontDelete 프로퍼티로 만들어지기 때문에 삭제될 수 없다:

// Global 변수:
var a = 1; // DontDelete가 설정된다.
delete a; // false
a; // 1

// Function:
function f() {} // DontDelete가 설정된다.
delete f; // false
typeof f; // "function"

// 다시 할당해도 삭제할 수 없다:
f = 1;
delete f; // false
f; // 1

명시적인(Explicit) 프로퍼티

다음 예제에서 만드는 프로퍼티는 delete할 수 있다. 이런 걸 명시적인(Explicit) 프로퍼티라고 부른다:

// Explicit 프로퍼티를 만든다:
var obj = {x: 1};
obj.y = 2;
delete obj.x; // true
delete obj.y; // true
obj.x; // undefined
obj.y; // undefined

obj.xobj.yDontDelete 속성이 아니라서 delete할 수 있다. 하지만 다음과 같은 코드도 잘 동작하기 때문에 헷갈린다:

// IE를 빼고 잘 동작한다:
var GLOBAL_OBJECT = this;
GLOBAL_OBJECT.a = 1;
a === GLOBAL_OBJECT.a; // true - 진짜 Global 변수인지 확인하는 것
delete GLOBAL_OBJECT.a; // true
GLOBAL_OBJECT.a; // undefined

this가 전역 객체를 가리키는 것을 이용해서 명시적으로 프로퍼티 a를 선언하면 삭제할 수 있다. 이것은 꼼수다.

IE (적어도 6-8)는 버그가 있어서 안 된다.

Argument들과 Function의 기본 프로퍼티

Function의 arguments 객체와 기본 프로퍼티도 DontDelete 속성이다.

// Function의 arguments와 프로퍼티:
(function (x) {

  delete arguments; // false
  typeof arguments; // "object"

  delete x; // false
  x; // 1

  function f(){}
  delete f.length; // false
  typeof f.length; // "number"

})(1);

Host 객체

Host 객체를 delete하면 어떻게 될지 알 수 없다. 표준에는 어떻게 Host 객체를 delete해야 하는지 정의하지 않았다.

결론

delete 연산자는 엉뚱하게 동작할 때가 많다. 명시적으로 정의한 일반 객체의 프로퍼티만 delete하는 것이 안전하다.

기타

setTimeoutsetInterval

JavaScript는 setTimeoutsetInterval함수를 이용해 비동기로 함수를 실행시킬수있다.

function foo() {}
var id = setTimeout(foo, 1000); // 0보다 큰 수를 반환한다.

setTimeout을 호출하면 타이머의 ID를 반환하고 대략 1,000밀리 초 후에 foo를 실행시킨다. foo딱 한 번만 실행한다.

JS엔진은 타이머에 설정한 시간(timer resolution)에 따라서 코드를 실행하지만 단일 쓰레드이기 때문에 특정 코드는 실행이 지연 될수도 있다. 따라서 setTimeout으로 코드가 실행돼야 할 시간을 정해줘도 정확하게 그 시간에 실행되지 않을수도 있다..

첫 번째 인자로 넘긴 함수는 전역 객체가 실행시킨다. 따라서 인자로 넘겨진 함수 내부의 this전역 객체를 가리키게 된다.

function Foo() {
    this.value = 42;
    this.method = function() {
        // this는 전역 객체를 가리키기 때문에 
        console.log(this.value); // undefined를 출력한다.
    };
    setTimeout(this.method, 500);
}
new Foo();

함수 호출을 쌓는(Stacking) setInterval함수.

setTimeout은 딱 한 번 함수를 호출하지만 setInterval은 이름처럼 지정한 시간마다 함수를 실행시켜준다. 하지만 이 함수의 사용은 좀 생각해봐야한다.

setInterval은 실행하는 코드가 일정시간 동안 블럭되도 계속해서 함수를 호출하기 때문에 주기가 짧은 경우 함수 호출이 쉽게 쌓여버린다.

function foo(){
    // 1초 동안 블럭함.
}
setInterval(foo, 1000);

위 코드에서 foo함수는 호출될 때마다 1초씩 실행을 지연시킨다.

하지만 foo함수가 블럭되더라도 setInterval함수는 계속해서 함수 호출을 쌓기 때문에 foo함수 호출이 끝나면 10번 이상의 함수 호출이 쌓여서 대기하고 있을수도 있다. (역주: 따라서 함수 호출이 쌓이게 되면 원래 기대했던 실행 주기를 보장받지 못한다.)

블럭되는 코드 해결법

앞에 문제를 해결하는 가장 쉽고 일반적인 방법은 setTimeout 함수에서 자기 자신을 다시 호출하는 방법이다.

function foo(){
    // something that blocks for 1 second
    setTimeout(foo, 1000);
}
foo();

이 방법은 함수 호출이 쌓이지도 않을 뿐만 아니라 setTimeout 호출을 해당 함수 안에서 관리하기 때문에 foo 함수에서 계속 실행할지 말지도 조절할 수 있다.

타이머 없애기

clearTimeoutclearInterval 함수로 setTimeout과 setInterval로 등록한 timeout과 interval을 삭제할 수 있다. set 함수들이 반환한 id를 저장했다가 clear 함수를 호출해서 삭제한다.

var id = setTimeout(foo, 1000);
clearTimeout(id);

모든 타이머 없애기

등록한 timeout과 interval을 한꺼번에 제거하는 내장 함수는 없다. 따라서 좀 무식하지만 직접 구현해야 한다.

// "모든" 타이머 지우기
for(var i = 1; i < 1000; i++) {
    clearTimeout(i);
}

위와 같은 방법은 숫자가 미치지 못하는 타이머는 여전히 남아있을수 있다는 단점이 있다. 또 다른 해결 방법은 타이머가 반환하는 값이 항상 전보다 1만큼 큰 수를 반환한다는 점을 착안한 방법이다.

// "모든" 타이머 지우기
var biggestTimeoutId = window.setTimeout(function(){}, 1),
i;
for(i = 1; i <= biggestTimeoutId; i++) {
    clearTimeout(i);
}

이 방법은 모든 주요 브라우저에서 문제없이 잘 동작하지만 ID가 항상 순차적이어야 한다고 표준에 명시된 것이 아니다. 그러므로 timeout ID를 모두 저장했다가 삭제하는 것이 가장 안전하다. 그러면 전부 깨끗하게 제거할 수 있다.

보이지 않게 사용되는 eval함수

setTimeoutsetInterval의 첫 파라미터로 문자열을 넘길 수 있다. 하지만 내부적으로 eval을 사용하는 것이기 때문에 절대 사용해서는 안된다.

function foo() {
    // 이게 호출됨
}

function bar() {
    function foo() {
        // 이것은 절대 호출 안 됨
    }
    setTimeout('foo()', 1000);
}
bar();

이 경우 eval그냥(directly) 호출되는 것이 아니다. setTimeout에 인자로 넘어간 문자열은 전역 스코프에서 실행되기 때문에 bar함수 영역에 있는 지역 변수 foo가 실행되는 것이 아니라 전역 스코프에 있는 foo가 실행된다.

함수에 파라미터를 넘겨야 하면 스트링을 사용하지 말아야 한다.

function foo(a, b, c) {}

// 절대 사용하면 안 됨
setTimeout('foo(1, 2, 3)', 1000)

// 대신 익명 함수를 사용하는 게 좋다.
setTimeout(function() {
    foo(a, b, c);
}, 1000)

결론

setTimeoutsetInterval함수에 문자열 인자를 절대 사용해서는 안된다. 핸들러 함수에 인자를 넘기는 코드도 절대 좋은 코드가 아니다. 익명 함수을 사용해서 호출해야 한다.

그리고 setInterval은 해당 핸들러가 블럭되든 말든 상관하지 않기 때문에 되도록이면 쓰지말자.