자바스크립트를 배우는 대부분의 사람들은 다른 언어를 배워본 경험이 있다. 자바스크립트는 다른 언어들과 다른 면들이 있는데, 이를 잘 이해하지 못하면, 자바스크립트를 잘 이용하기가 어렵다. 우리들만의 잘못은 아니다. 자바스크립트는 scheme 처럼 동작하도록 설계되었는데, 마치 c 처럼 생겼기 때문이다. 이 문서는 lexical scope 과 this 변수에 대해서 설명하고, 이들을 자바스크립트에서 어떻게 사용해야하는지에 대해서 얘기할 것이다.
It's all about where you are.
모든 프로그래밍 언어에는, 현재 scope 와 현재 context 라는 개념이 있다. 자바스크립트에는 lexical scope 과 this 라는 context 가 있다.
자바스크립트에서는 모든 새로운 scope 들은 function 의 정의로 인해 생성된다. 하지만 C 같은 언어와는 다르게, 이것이 scope 를 만드는 유일한 방법이다. 반복문도, if 문도, {} 블락 도 scope 를 만들지 않는다. 이런 단순함은 축복임과 동시에 저주다. 예를들어 scope 를 생성하는 것을 설명해보겠다.
global scope 의 예이다:
// global 변수를 선언한다.
var name = "Tim";
var age = 28;
// global scope 에서 이들을 접근한다.
name;
Output
=> 'Tim'
local scope 의 예이다:
// 함수에서 local 변수들을 생성한다.
function newScope() {
var name = "tim";
var age = 28;
}
// global scope 에서 local 변수를 접근하려 하면
// 에러가 발생한다.
name;
Error
ReferenceError: name is not defined
Lexical Scope
Lexical scope 은 closure 를 가능하게하는 핵심 요소이다. wikipedia 의 closure 와 lexical scope 에 대한 글이다:
컴퓨터 공학에서, closure 는, lexical 환경에 bound 된 free 변수를 갖는 first-class 함수이다. 이런 함수는 free 변수들을 "closed over" 한다고 표현한다. 클로저는 이들 free 변수들의 scope 내에 정의된다. 그리고 이 변수들은 closure 가 살아있는 동안 살아있게 된다.
이게 무슨 소리인가? 다음 예제를 보라:
var name = "outer";
function one() {
var name = "middle";
var other = "findme";
function two() {
var name = "inner";
// `name` 은 "inner" 이고, `other` 는 "findme" 이다
console.dir({name: name, other: other});
}
two();
// `name` 은 "middle" 이고 `other` 는 "findme" 이다
console.dir({name: name, other: other});
}
one();
// `name` 은 "outer" 이고 `other` 는 undefined 이다
console.dir({name: name});
console.dir({other: other});
안쪽 scope 에 있는 local 변수가 바깥 scope 에 있는 같은 이름의 변수를 가려버린다는 것을 알 수 있다. 그리고 바깥쪽에서는 안쪽의 변수를 볼 수가 없다. lexical scope 이란 변수의 scope 이, 코드에서의 변수 위치에(얼마나 안쪽이냐) 따라 결정되는 것을 말한다. 안쪽 함수에 접근하기 위해 어떤 경로를 거치는 것과는 아무 상관이 없다.
// closure 함수를 리턴하는 함수를 만든다.
function myModule() {
var name = "tim", age = 28;
return function greet() {
return "Hello " + name + ". Wow, you are " + age + " years old.";
}
}
// `myModule()` 을 호출하여 closure 를 얻는다.
var greeter = myModule();
// closure 를 호출한다
greeter();
Output
=> 'Hello tim. Wow, you are 28 years old.'
name 과 age 변수는 myModule 함수의 로컬 변수이다. 하지만 글로벌 scope 에서 greeter() 를 호출할 때 에러가 발생하지 않는다. 그 이유는 greet 함수가 name 과 age 를 자신의 lexical scope 에 가지기 때문이다. 따라서 마치 이 변수들이 자신의 local 변수인양 사용할 수가 있다. 기본적으로 변수를 찾을 때에는, 자신의 scope 에서 먼저 찾고 그 위의 scope 들로 차례 차례 찾는다.
The context of "this"
자바스크립트는 this 라는 특별한 키워드를 이용한다. this 는 다른 여타 변수들과 동일하게 동작하나, 수정이 불가능하다는 점이 다르다. 이 변수는 context (역주: 환경) 을 가리키는 참조 객체로 쓰인다. 객체이기 때문에, . 이나 [] 을 이용하여 속성들을 접근할 수도 있다. 신기한 점은 여러분이 함수를 호출하는 context 에 따라 this 의 값이 달라진다는 것이다. 보통의 경우에는 context 는 message 를 받는 (역주: 호출받는) 객체이다. 예를들어:
var Person = {
name: "Tim",
age: 28,
greeting: function () {
return "Hello " + this.name + ". Wow, you are " + this.age + " years old.";
}
};
Person.greeting();
Output
=> 'Hello Tim. Wow, you are 28 years old.'
Person.greeting 에서 Person.name 과 Person.age 를 접근하였다.
"this" is where it bites
위 코드는 다른 언어의 클래스 객체처럼 보인다. 하지만 거기에 함정이 있다. this 가 꼭 Person 일 것이라는 보장이 없다. 예를들어 greeting 함수를 다음처럼 저장한다고 해보자:
var greeting = Person.greeting;
greeting(); // `this.name` 과 `this.age` 는 undefined 가 된다
Output
=> 'Hello undefined. Wow, you are undefined years old.'
위 greeting 함수에서 this 는 global 객체이지 Person 객체가 아니다. 또 다른 예를 보자:
var Dog = {
name: "Alfred",
age: 110,
greeting: Person.greeting
}
Dog.greeting(); // 정상작동하며, Dog 의 데이터를 보여준다.
Output
=> 'Hello Alfred. Wow, you are 110 years old.'
Dog 과 Person 의 greeting 함수는 같은 함수이다. 이 두 함수는 메모리상의 같은 객체를 참조한다. 하지만 어디서 불렸는지에 따라 this 의 값이 변할 수 있다. 기본적으로 함수호출시 . 앞에 있는 객체가, 함수에서 this 가 된다. Dog.greeting() 에서는 this 가 Dog 이고, Person.greeting() 에서는 Person 이다. . 앞에 아무것도 없을 때에는 글로벌 객체가 this 가 된다.
Taming "this"
자바스크립트는 call 과 apply 라는 Function.prototype 의 함수들을 제공한다. 거의 기능이 같은데 인자를 다른 형태로 받는다. 이전 예제를 이어서, greeting 이 없는 새로운 객체를 만들어 보자. 하지만 이 객체로 greeting 함수를 호출할 수 있다.
var Alien = {
name: "Zygoff",
age: 5432
}
Person.greeting.call(Alien);
Output
=> 'Hello Zygoff. Wow, you are 5432 years old.'
여기서 우리는 Person.greeting 함수를 부르면서 this 로 Alien 을 쓰도록 하는 것이다. call 대신 apply 를 써도 무방하다. 전달하는 인자가 없기 때문이다.
age 와 name 속성을 갖는 아무 객체나 사용할 수 있는 함수를 만들어보자:
function makeOlder(years, newname) {
this.age += years;
if (newname) {
this.name = newname;
}
}
이 함수는 this 객체에 years 를 더하고, 선택적으로 이름도 변경한다. 이 함수는 어떤 특정 객체와 연결되어 있지 않다. (바깥 scope 의 변수를 사용하지 않는 함수는 독립적인 함수이다). call 이나 apply 를 이용하여 위 함수를 다음과 같이 이용한다:
makeOlder.call(Person, 2, "Old Tim");
makeOlder.apply(Dog, [1, "Shaggy"]);
console.dir({Person: Person, Dog: Dog});
call 은 전달받은 인자들을 하나씩 함수로 전달된다. apply 는 배열 인자를 전달받아 함수로 전달한다.
Binding "this"
때때로, 우리는 OOP (객체지향프로그래밍) 스타일을 너무 좋아한 나머지, 자바스크립트도 OOP 처럼 동작하게 만들려 한다. 어떻게 함수를 호출하느냐에 따라 this 가 변한는 것이 싫다. 콜백 함수를 인자로 받는 이벤트를 설정할 때 이와 같은 일이 많이 발생한다.
Cart = {
items: [1,4,2],
onClick: function () {
// this.items 로 무엇인가를 한다.
}
}
$("#mybutton").click(Cart.onClick);
문제 없어 보이지만, 재앙이 될 수 있는 코드다. onClick 이 아닌 Cart.onClick 를 사용하더라도 말이다. jQuery click 함수는 Cart.onClick 함수를 받지만, 이 함수를 호출할 때에는, 이 함수가 Cart 로부터 왔다는 것을 모를 것이다. (역주: Cart.onClick() 이 아닌 var eventFunc = Cart.onClick; eventFunc() 인 셈이기 때문이다)
closure 와 lexical scope 지식을 이용하여, this 가 다른 OOP 언어에서처럼 동작하게 해보자.
$("#mybutton").click(function () { Cart.onClick() });
closure 를 만들고 Cart.onClick() 을 호출한다. 문제는 (읽기 어렵다는 것외에도) 함수가 인자를 받지도 값을 리턴하지도 않는다는 것이다.
$("#mybutton").click(function () { return Cart.onClick.apply(Cart, arguments) });
이렇게 하면 되지만, 더 읽기 어려운 코드가 되었다. arguments 는 현재 가장 안쪽의 함수에게 전달된 인자들을 가지고 있는 배열이다.
Cart 가 전역적으로 접근 가능한 singleton 객체라면, this 대신에 Cart 를 사용할 수 있을 것이다. 하지만 그렇지 않은 경우가 더 많다.
Cart.onClick 의 this 가 항상 Cart 를 가리키게 할 수는 없을까?
function bind(fn, scope) {
return function () {
return fn.apply(scope, arguments);
}
}
Cart.onClick = bind(Cart.onClick, Cart);
$("#mybutton").click(Cart.onClick);
이 방법말고도 다른 방법들이 있지만, 보통 올바른 해결책은 아니다. javascript 를 다른 언어처럼 만들고 싶다면, 좋은 방법이지만, javascript 의 방법을 배우는 것이 더 좋다.
위에서 우리는 scope 을 포함하는 closure 를 만들었다. (역주: scope 보다는 context 가 이 글에서의 쓰임새에 더 맞는것이 아닌가 싶다) 그리고 Cart.onClick 을 이 closure 로 변경 하였다. 그리고 apply 를 이용하여 인자를 전달하고 값을 리턴하였다.
Var statements
var 문장은 단순히 어떤 변수가 어떤 scope 에 속하는지를 나타내는 키워드이다. var 를 사용하지 않는다면, 모든 변수는 글로벌 변수가 될 것이다.
global_var = true;
function someFunc() {
another_global = 42;
var local_var = 5;
}
다음 같은 반복문에서 매우 위험하다:
function sum(start, end) {
var n = 0;
for (i = start; i <= end; i++) {
n += i;
}
return n;
}
function nested_sum(num) {
var n = 0;
for (i = 1; i <= num; i++) {
n += sum(1, i);
}
return n;
}
nested_sum(3); // 10 을 원하지만, i 값이 꼬인다.
Output
=> 7
두개의 룹에서 i 변수는 같은 변수로, 안쪽에서 i 가 증가하여 바깥의 룹에 영향을 미치게 된다. (역주: (1) + (1+2+3) = 7) 또 n 앞에 var 를 붙이지 않아도, 서로 간섭하여 잘못된 값을 리턴할 것이다.
var 를 사용할 때 주의점이 있다. 함수 어디에 var 를 놓아도 상관없다. 어떤 함수에 var 를 놓느냐만 중요하다. 그리고 이렇게 선언한 변수의 scope 은 함수의 인자들 보다 안쪽이다 (역주: 응?)
name = "Tim";
function greet() {
console.dir(name);
// name 은 undefined 이다.
// 코드 아래 쪽에 var 로 변수를 선언하였기 때문이다
var name;
}
greet();
function greet2(name) {
console.dir(name);
// name 은 "Tim" 이다. 왜냐하면 함수의 인자들은 특별하기 때문이다.
// 한참 후에, var name 이 있더라도 말이다
var name;
}
greet2("Tim");
Output
undefined 'Tim'
이것이 jslint 이 모든 var 선언을 함수의 젤 위에 놓으라는 이유이다. 나는 한때 이문제로 골머리를 썩인적이 있다.
자바스크립트에서 scope 을 이해하려면 다음을 이해해야 한다:
•새로운 scope 은 function 을 통해서만 만들 수 있다. for ... in 등 다른 어떤 것도 scope 을 만들지 않는다.
•var 문장은 변수를 현재의 scope 에 로컬 변수로 선언한다. var 문장을 사용한 이후가 아닌, var 문장을 포함하는 scope 에 선언하는 것이다. 이 로컬 변수는 바깥 scope 의 같은이름 변수를 가린다.
•this 와 arguments 를 제외한 모든 변수는 lexical scope 을 따른다. 즉 이들 변수의 의미는 코드상의 위치 에 따라 정해진다.
•this 와 arguments 변수는 모든 중첩 마다 변화한다. 이 변수들을 closure 에 보관하려면, lexical scope 을 따르는 다른 변수를 이용하여 this 나 arguments 들을 참조하여야 한다.
•this 의 값은 함수가 호출된 방법 에 의해 정해진다. apply 와 call 을 이용하여 이를 조절할 수 있다.
위 룰에 예외가 있다. function decomposition, eval, with 키워드를 이용할 때이다. (역주: 움 뭔지 모르는 것들이다) 이 때에도, 위 룰들은 따르지만, 예상과는 다른 방법일 것이다. function decompilation + eval 은 함수를 새로운 lexical scope 으로 이동시킨다. 그리고 with 를 이용하여 this.name 이 local lexical 변수인 것처럼 만들 수도 있다.