JavaScript 언어의 핵심에 대한 내용을 모아 만든 JavaScript Garden
Intro
JavaScript 언어의 핵심에 대한 내용을 모아 JavaScript Garden을 만들어 었다. 이 글이 초보자가 JavaScript 익히면서 자주 겪는 실수, 미묘한 버그, 성능 이슈, 나쁜 습관들 줄일 수 있도록 도와줄 것이다.
JavaScript Garden은 단순히 JavaScript 언어 자체를 설명하려 만들지 않았다. 그래서 이 글에서 설명하는 주제들을 이해하려면 반드시 언어에 대한 기본 지식이 필요하다. 먼저 Mozilla Developer Network에 있는 문서로 JavaScript 언어를 공부하기 바란다.
저자들
이 글은 Stack Overflow에서 사랑받는 두 사람 Ivo Wetzel과 Zhang Yi Jiang의 작품이다. Ivo Wetzel이 글을 썼고 Zhang Yi jiang이 디자인을 맡았다.
기여자들
번역
호스팅
JavaScript Garden은 Github에서 호스팅하고 있고 Cramer Development가 JavaScriptGarden.info에서 미러링해주고 있다.
저작권
JavaScript Garden은 MIT license를 따르고 GitHub에서 호스팅하고 있다. 문제를 발견하면 이슈를 보고하거나 수정해서 Pull Request를 하라. 아니면 Stack Overflow 채팅 사이트의 Javascript room에서 우리를 찾으라.
객체
객체와 프로퍼티
JavaScript에서 null
과 undefined
를 제외한 모든 것들은 객체처럼 동작한다.
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
를 사용해야만 한다. 프로퍼티에 undefined
나 null
을 할당하는 것은 프로퍼티를 삭제하는 것이 아니라 프로퍼티에 할당된 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 undefined
와 foo 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.prototype
과 Foo.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.prototype
의 hasOwnProperty
메소드를 이용해 본래 객체의 프로퍼티만 골라낸다.
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();
이때에도 this
는 Global 객체를 가리킨다.
메소드로 호출할 때
test.foo();
이 경우에는 this
가 test
를 가리킨다.
생성자를 호출할 때
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.prototype
의 call
이나 apply
메소드를 호출하면 this
가 무엇을 가리킬지 정해줄 수 있다. 호출할 때 첫 번째 인자로 this
가 가리켜야 할 객체를 넘겨준다.
그래서 foo
Function 안에서 this
는 위에서 설명했던 객체 중 하나를 가리키는 것이 아니라 bar
를 가리킨다.
대표적인 함정
this
가 Global 객체를 가리키는 것도 잘못 설계된 부분 중 하나다. 괜찮아 보이지만 실제로는 전혀 사용하지 않는다.
Foo.method = function() {
function test() {
// 여기에서 this는 Global 객체를 가리킨다.
}
test();
}
test
에서 this
가 Foo
를 가리킬 것으로 생각할 테지만 틀렸다. 실제로는 그렇지 않다.
test
에서 Foo
에 접근하려면 method에 Local 변수를 하나 만들고 Foo
를 가리키게 하여야 한다.
Foo.method = function() {
var self = this;
function test() {
// 여기에서 this 대신에 self를 사용하여 Foo에 접근한다
}
test();
}
self
는 통상적인 변수 이름이지만, 바깥쪽의 this
를 참조하기 위해 일반적으로 사용된다. 또한 클로저와 결합하여 this
의 값을 주고 받는 용도로 사용할 수도 있다.
ECMAScript 5부터는 익명 함수와 결합된 bind
메소드를 사용하여 같은 결과를 얻을 수 있다.
Foo.method = function() {
var test = function() {
// this는 이제 Foo를 참조한다
}.bind(this);
test();
}
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
여기서 Counter
는 increment
클로저와 get
클로저 두 개를 반환한다. 이 두 클로저는 Counter
함수 스코프에 대한 참조를 유지하고 있기 때문에 이 함수 스코프에 있는 count 변수에 계속 접근할 수 있다.
Private 변수의 동작 원리
JavaScript에서는 스코프(Scope)를 어딘가에 할당해두거나 참조할수 없기 때문에 스코프 밖에서는 count 변수에 직접 접근할 수 없다. 접근할수 있는 유일한 방법은 스코프 안에 정의한 두 클로저를 이용하는 방법밖에 없다.
var foo = new Counter(4);
foo.hack = function() {
count = 1337;
};
위 코드에서 foo.hack
함수는 Counter 함수 안에서 정의되지 않았기 때문에 이 함수가 실행되더라도 Counter
함수 스코프 안에 있는 count 값은 변하지 않는다. 대신 foo.hack 함수의 count
는 Global 스코프에 생성되거나 이미 만들어진 변수를 덮어쓴다.
반복문에서 클로저 사용하기
사람들이 반복문에서 클로저를 사용할 때 자주 실수를 하는 부분이 있는데 바로 인덱스 변수를 복사할때 발생한다.
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)
}
즐겨 쓰이는 또 하나의 방법은 setTimeout
함수에 세번째 인자를 추가하는 방법이다. 추가된 인자는 콜백 함수에 전달된다.
for(var i = 0; i < 10; i++) {
setTimeout(function(e) {
console.log(e);
}, 1000, i);
}
레거시 JS 환경(Internet Explorer 9 이하)은 이 방법을 지원하지 않는다.
.bind
를 사용하는 방법도 있다. .bind
는 this
컨텍스트와 인자들을 함수에 결속(bind)시킨다. 아래 코드는 위 코드와 동일하게 동작한다.
for(var i = 0; i < 10; i++) {
setTimeout(console.log.bind(console, 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) {
// 내곡동에 땅이라도 산다.
}
또 다른 트릭은 call
과 apply
를 함께 사용하여 메써드(this
의 값과 인자들을 사용하는 함수)를 단지 인자들만 사용하는 일반 함수로 바꾸는 것이다.
function Person(first, last) {
this.first = first;
this.last = last;
}
Person.prototype.fullname = function(joiner, options) {
options = options || { order: "western" };
var first = options.order === "western" ? this.first : this.last;
var last = options.order === "western" ? this.last : this.first;
return first + (joiner || " ") + last;
};
// "fullname" 메써드의 비결합(unbound) 버전을 생성한다.
// 첫번째 인자로 'first'와 'last' 속성을 가지고 있는 어떤 객체도 사용 가능하다.
// "fullname"의 인자 개수나 순서가 변경되더라도 이 랩퍼를 변경할 필요는 없을 것이다.
Person.fullname = function() {
// 결과: Person.prototype.fullname.call(this, joiner, ..., argN);
return Function.call.apply(Person.prototype.fullname, arguments);
};
var grace = new Person("Grace", "Hopper");
// 'Grace Hopper'
grace.fullname();
// 'Turing, Alan'
Person.fullname({ first: "Alan", last: "Turing" }, ", ", { order: "eastern" });
일반 파라미터와 arguments 객체의 인덱스
일반 파라미터와 arguments
객체의 프로퍼티는 모두 getter와 setter를 가진다.
그래서 파라미터나 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 객체를 안쓰겠다는 의미니까 상관 없다.
그리고 getter와 setter는 항상 생성되기 때문에 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 Person(name) {
this.name = name;
}
Person.prototype.logName = function() {
console.log(this.name);
};
var sean = new Person();
위 코드는 Person
을 생성자로 호출하고 새로 생성된 객체의 prototype
을 Person.prototype
으로 설정한다.
아래 코드와 같이 생성자에 명시적인 return
문이 있는 경우에는 반환하는 값이 객체인 경우에만 그 값을 반환한다.
function Car() {
return 'ford';
}
new Car(); // 'ford'가 아닌 새로운 객체를 반환
function Person() {
this.someValue = 2;
return {
name: 'Charles'
};
}
new Person(); // someValue가 포함되지 않은 ({name:'Charles'}) 객체 반환
new 키워드가 없으면 그 함수는 객체를 반환하지 않는다.
function Pirate() {
this.hasEyePatch = true; // 전역 객체를 준비!
}
var somePirate = Pirate(); // somePirate = undefined
위 예제는 그때그때 다르게 동작한다. 그리고 this
객체의 동작 원리에 따라서 Foo 함수안의 this
의 값은 Global 객체를 가리키게된다. (역주: 결국 new 키워드를 빼고, 코드를 작성할 경우 원치 않은 this 참조 오류가 발생할 수 있다.)
팩토리
생성자가 객체를 반환하면 new
키워드를 생략할 수 있다.
function Robot() {
var color = 'gray';
return {
getColor: function() {
return color;
}
}
}
Robot.prototype = {
someFunction: function() {}
};
new Robot();
Robot();
new 키워드의 유무과 관계없이 Robot
생성자의 동작은 동일하다. 즉 클로저가 할당된 method 프로퍼티가 있는 새로운 객체를 만들어 반환한다.
new Robot()
으로 호출되는 생성자는 반환되는 객체의 prototype 프로퍼티에 아무런 영향을 주지 않는다. 객체를 반환하지 않는 생성자로 만들어지는 경우에만 객체의 prototype이 생성자의 것으로 할당된다.
그러니까 이 예제에서 new
키워드의 유무는 아무런 차이가 없다. (역주: 생성자에 객체를 만들어 명시적으로 반환하면 new 키워드에 관계없이 잘 동작하는 생성자를 만들수있다. 즉, new 키워드가 빠졌을때 발생하는 this 참조 오류를 방어해준다.)
팩토리로 객체 만들기
new
키워드를 빼먹었을 때 버그가 생긴다는 이유로 아예 new를 사용하지 말 것을 권하기도 한다.
객체를 만들고 반환해주는 팩토리를 사용하여 new
키워드 문제를 회피할 수 있다.
function CarFactory() {
var car = {};
car.owner = 'nobody';
var milesPerGallon = 2;
car.setOwner = function(newOwner) {
this.owner = newOwner;
}
car.getMPG = function() {
return milesPerGallon;
}
return car;
}
new
키워드가 없어도 잘 동작하고 private 변수를 사용하기도 쉽다. 그렇지만, 단점도 있다.
- prototype으로 메소드를 공유하지 않으므로 메모리를 좀 더 사용한다.
- 팩토리를 상속하려면 모든 메소드를 복사하거나 객체의 prototype에 객체를 할당해 주어야 한다.
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는 다음과 같은 순서로 찾는다.
- 해당 Scope에서
var foo
구문으로 선언된 것을 찾는다. - Function 파라미터에서
foo
라는 것을 찾는다. - 해당 Function 이름이
foo
인지 찾는다. - 상위 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 arr = [1, 2, 3, 4, 5, 6];
arr.length = 3;
arr; // [1, 2, 3]
arr.length = 6;
arr.push(4);
arr; // [1, 2, 3, undefined, undefined, undefined, 4]
현재 크기보다 더 작은 값을 할당하면 배열을 자른다. 배열의 크기를 증가시키면 드문드문(sparse)한 배열을 생성한다.
결론
최적의 성능을 위해서는 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
위 표에서 Type은 typeof
가 반환하는 값이다. 위 표에서처럼 일치되는 값이 거의 없다.
위 표에서 Class는 객체 내부에 있는 [[Class]]
프로퍼티의 값을 말한다.
[[Class]]
프로퍼티의 값을 가져다 쓰려면 Object.prototype
의 toString
메소드를 사용한다.
객체의 클래스
표준에 의하면 [[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.toString
은 this의 [[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 시스템 때문에 생기는 많은 문제가 남아있다.
기본 타입 생성자
Number
나 String
같은 기본 타입들의 생성자는 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 number = 1;
function test() {
var number = 2;
eval('number = 3');
return number;
}
test(); // 3
number; // 1
eval
함수는 eval
이라는 이름으로 직접 실행할 때에만 지역 스코프에서 실행된다. 그리고 eval
이라는 이름에 걸맞게 악명또한 높다.
var number = 1;
function test() {
var number = 2;
var copyOfEval = eval;
copyOfEval('number = 3');
return number;
}
test(); // 2
number; // 3
어쨌든 eval
은 사용하지 말아야 한다. eval을 사용하는 99.9%는 사실 eval 없이도 만들수있다.
가짜 eval
setTimeout
과 setInterval
은 첫 번째 인자로 스트링을 입력받을 수 있다. 이 경우에는 eval
을 직접 호출하는 것이 아니라서 항상 Global Scope에서 실행된다.
보안 이슈
eval
은 어떤 코드라도 무조건 실행하기 때문에 보안 문제도 있다. 따라서 신뢰하지 못하거나 모르는 코드가 포함되어 있을 경우 절대로 사용해서는 안된다.
결론
eval
은 사용하지 않는 게 좋다. eval
을 사용하는 모든 코드는 성능, 보안, 버그 문제를 일으킬 수 있다. 만약 eval
이 필요해지면 설계를 변경하여 eval
이 필요 없게 만들어야 한다.
undefined
와 null
JavaScript는 nothing
을 표현할때 null
과 undefined
두 가지로 표현할 수 있고 그중 undefined
가 더 유용하다.
undefined
도 변수
undefined
는 undefined
라는 값을 가지는 데이터 형식이다.
undefined
는 상수도 아니고 JavaScript의 키워드도 아니다. 그냥 undefined
라는 이름의 Global 변수이고 이 변수에는 undefined
라고 할당돼 있다. 그래서 이 Global 변수의 값을 쉽게 바꿀 수 있다.
undefined
값이 반환될 때:
- global 변수
undefined
에 접근할 때. - 선언은 했지만 아직 초기화하지 않은 변수에 접근할 때.
return
구문이 없는 함수는 암묵적으로undefined
를 반환함.return
구문으로 아무것도 반환하지 않을 때.- 없는 프로퍼티를 찾을 때.
- 함수 인자가 생략될 때.
undefined
가 할당된 모든 것.void(expression)
형식으로 된 표현
undefined
가 바뀔 때를 대비하기
global 변수 undefined
는 undefined
라는 객체를 가리키는 것뿐이기 때문에 새로운 값을 할당한다고 해도 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.x
와 obj.y
는 DontDelete
속성이 아니라서 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하는 것이 안전하다.
기타
setTimeout
과 setInterval
JavaScript는 setTimeout
과 setInterval
함수를 이용해 비동기로 함수를 실행시킬수있다.
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, 100);
위 코드에서 foo
함수는 호출될 때마다 1초씩 실행을 지연시킨다.
하지만 foo
함수가 블럭되더라도 setInterval
함수는 계속해서 함수 호출을 쌓기 때문에 foo
함수 호출이 끝나면 10번 이상의 함수 호출이 쌓여서 대기하고 있을수도 있다. (역주: 따라서 함수 호출이 쌓이게 되면 원래 기대했던 실행 주기를 보장받지 못한다.)
블럭되는 코드 해결법
앞에 문제를 해결하는 가장 쉽고 일반적인 방법은 setTimeout
함수에서 자기 자신을 다시 호출하는 방법이다.
function foo(){
// something that blocks for 1 second
setTimeout(foo, 100);
}
foo();
이 방법은 함수 호출이 쌓이지도 않을 뿐만 아니라 setTimeout
호출을 해당 함수 안에서 관리하기 때문에 foo
함수에서 계속 실행할지 말지도 조절할 수 있다.
타이머 없애기
clearTimeout
과 clearInterval
함수로 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
함수
setTimeout
과 setInterval
의 첫 파라미터로 문자열을 넘길 수 있다. 하지만 내부적으로 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(1, 2, 3);
}, 1000)
결론
setTimeout
과 setInterval
함수에 문자열 인자를 절대 사용해서는 안된다. 핸들러 함수에 인자를 넘기는 코드도 절대 좋은 코드가 아니다. 익명 함수을 사용해서 호출해야 한다.
그리고 setInterval
은 해당 핸들러가 블럭되든 말든 상관하지 않기 때문에 되도록이면 쓰지말자.
출처: http://bonsaiden.github.io/JavaScript-Garden/ko/