티스토리 뷰


* 이번에는 많은 개발자들이 처음으로 자바스크립트를 접하고 가장 헷갈려하는 this가 결정되는 방법에 대해서 공부를 해보자.


- 이전 글

2012/12/10 - [속깊은 자바스크립트 강좌] 시작 (예고편)

2012/12/17 - [속깊은 자바스크립트 강좌] 자바스크립트의 Scope와 Closure 기초

2013/01/07 - [속깊은 자바스크립트 강좌] function declaration vs function expression 차이점



* 함수(function)의 호출

: this가 어떻게 결정되는지 알려면 일단 먼저 함수를 호출하는 방법에 대하여 알아보아야한다. 함수의 호출은 크게 4가지로 분류할 수 있다. 이 4가지에 대하여 아주 간단하게 어떻게 호출되는지 알아보고 각 경우에 대하여 this가 결정되는 방법을 알아보자. 아래가 그 4가지 이다.

    • #1. 일반 함수로의 호출
    • #2. 멤버 함수로의 호출
    • #3. call 함수를 이용한 호출
    • #4. apply 함수를 이용한 호출

- #1 일반 함수로의 호출은 이전부터 사용해왔던 함수 호출의 방법이다. 아래의 #1처럼 간단하게 함수 뒤에 괄호를 붙여서 호출하는 것이다.


function say(something) {
    alert(something);
}
say("ho");    // #1


- 멤버 함수로의 호출은 함수가 다른 객체의 멤버함수로 있을 때에 호출하게 되는 방법이다. 객체나 라이브러리를 활용하게 될 때 자주 이용하게 되는 방법이다. 이것은 함수가 객체의 멤버 변수로 설정이 되었을 때 호출하는 방법으로 아래의 #2와 같이 다른 언어에서도 자주 이용해봤을 것이다.


var unikys = { 
    say: function (something) {
        alert(something);
    }
}
unikys.say("hello");    //  #2


- call과 apply 함수는 자바스크립트만의 독특한 내장 함수로, Function 객체에 기본적으로 들어있는 함수이다. 정확히 말하자면 Function.prototype에 들어있는 함수이다. 브라우져의 개발자 콘솔을 열어서 Function.prototype.call과 Function.prototype.apply를 쳐보면 내장 함수로 되어있는 것을 확인할 수 있다.

 

 

call과 apply는 내장함수 ( [native code] )로 되어있다

: 위의 #1이나 #2처럼 function 키워드로 함수를 생성하게 되면 그것을 바탕으로 Function 객체를 생성하게 되고, 그렇게 생성된 함수에 이러한 call과 apply 함수를 기본적으로 사용할 수 있다.

function say(something) {
    alert(something);
}
say.call(undefined, "call ho");    // #3
say.apply(undefined, ["apply ho"]);    //  #4

 

: 위의 예에서 call과 apply의 다른점은 이후의 인자를 직접 객체로 하나하나 넘겨주느냐, 또는 배열로 넘겨주느냐의 차이이다. 지금 당장은 이들 둘의 차이를 크게 느끼기 힘들겠지만, 둘은 약간 다르게 활용이 가능하다.

 

: 일단 함수를 호출하는 방법들을 알아봤는데, 이제는 각 방법에서 this가 결정되는 방법에 대하여 알아보자.

 

* 호출 방법에 따라 this가 결정되는 방법

: 아래의 간단한 예제함수를 통하여 각각의 호출 방법에 따라 달라지는 것을 관찰해보자.

function whatsThis() {
    return this.toString();
}
var unikys = {
    what: whatsThis,
    toString: function () {
        return "[object unikys]";
    }
};
whatsThis();    // #1
unikys.what();    //  #2
whatsThis.call();    //  #3
whatsThis.apply(unikys);    // #4
unikys.what.call(undefined);    //  #5
unikys.what.apply(unikys);    //  #6

 

: unikys객체 안에는 Object의 기본 내장 함수인 toString을 오버라이트해서 "[object unikys]"를 리턴하도록 해서 어떠한 object인지 명확하게 알려주도록 설정하고 위의 4가지 경우에 대하여 개발자 콘솔에서 아래와 같이 실행해봤다.

 

 

 

: 각 호출 방법에 의해 this가 결정된 것을 보면 다음과 같다.

 

  • #1: this = window
  • #2: this = unikys
  • #3: this = window
  • #4: this = unikys
  • #5: this = window
  • #6: this = unikys

: 하나씩 살펴보면 #1인 경우는 글로벌 window 객체가 this로 설정되었다. 이렇게 #1처럼 호출할 때에는 숨은 인자가 있어서 그 인자는 바로 this가 되는 것인데, 숨은 인자는 기본값으로 window가 들어가게 된다 (ECMAScript5의 strict 모드에서는 이 부분 동작이 다른데, 조금 뒤에 알아보자). 나머지 다른 호출 방법에 의해 함수를 호출하게 되면 바로 이 숨은 인자가 다르게 설정되는 것이다. 이에 대한 정확하게 돌아가는 내용은 나머지 호출 방법들을 살펴보고 ECMAScript 5의 스펙에서 살펴보자.

 

: 그럼 #2의 경우는 unikys.what()으로 호출한 경우는 이 숨은 인자로 unikys 객체가 넘어가는 것이다. 따라서 #2인 경우에는 this가 멤버변수로 호출한 객체가 되는 것이다. 하지만 같은 함수더라도 호출하는 방법이 다르면 this가 또 바뀌게 된다. 위의 예에서 아래의 소스를 실행하면 다시 this가 바뀌게 되는 것을 확인할 수 있다.

var newWhat = unikys.what;
newWhat();    // === "[object Window]"


: 이렇게 this가 함수나 scope 기반으로 결정되는 것이 아니라 호출 방법에 의하여 결정되다보니 많은 C나 자바 개발자들은 엄청난 혼란의 도가니에 빠질 것이다. 하지만 이 개념은 지금 이러한 숨은 인자의 존재만 잘 이해하고 있으면 제대로 활용할 수 있을 것이다.

 

: 이제 #3번의 경우를 살펴보면, Function의 기본 내장 함수인 call 함수로 호출하고 있는데 인자가 없다. 자바스크립트에서는 함수에 인자가 부족해도, 많아도 호출하는데 전혀 지장이 없고, 만약에 인자가 적다면 기본값으로 undefined가 들어가게 된다. 이 이야기를 왜 하느냐하면, 바로 call함수와 apply 함수인 경우 위에서 #1과 #2에서 말한 this를 나타내는 '숨은 인자'가 1번째 인자로 나오게 되기 때문이다. 즉, call함수와 apply 함수의 첫번째 인자가 바로 this가 되는 것이다. 만약 첫번째 인자에 undefined나 null을 넘겨주거나 아무 인자 없이 호출하게 되면 #1에서 호출하는 것과 마찬가지인 것이다. 즉, #1과 #2를 각각 call 함수로 나타내보면 아래와 같이 되는 것이다.

whatsThis() === whatsThis.call(undefined);    //  === whatsThis.call();
unikys.what() === unikys.what.call(unikys);

 

: 위와 같이 참수를 호출할 때에 call이나 apply 함수의 첫번째 인자를 자동으로 설정해주는 것과 같은 동작을 하게 되는 것이다. 이렇게 보면 #4, #5, 그리고 #6의 동작하는 방법을 이해할 수 있을 것이다. call과 apply를 이용하게 되면 첫번째 인자가 this가 되는 것이다.

 

 

* 내부 동작

: 이번에는 내부적으로 돌아가는 것으로 더 명확하게 알아보자. 이전 편에서 가져왔던 함수가 호출 되었을 때 작동하는 내부 프로세스를 살펴보자.

 

1. If the function code is strict code, set the ThisBinding to thisArg.

2. Else if thisArg is null or undefined, set the ThisBinding to the global object.

3. Else if Type(thisArg) is not Object, set the ThisBinding to ToObject(thisArg).

4. Else set the ThisBinding to thisArg.

5. Let localEnv be the result of calling NewDeclarativeEnvironment passing the value of the [[Scope]] internal property of F as the argument.

6. Set the LexicalEnvironment to localEnv.

7. Set the VariableEnvironment to localEnv.

8. Let code be the value of F’s [[Code]] internal property.

9. Perform Declaration Binding Instantiation using the function code code and argumentList as described in 10.5.

 

: 이번에는 1~4단계만 살펴보면 될 것이다. 위에서 thisArg를 이전에 말한 숨은 인자로 생각하고 읽으면 이해가 될 것이다. 이제부터는 위의 숨은 인자를 thisArg로 바꿔서 쓰겠다.

  1. 현재 함수가 strict 모드면 일단 this를 thisArg(숨은 인자)로 설정한다.
  2. strict모드가 아니고, thisArg가 undefined나 null이면 this를 글로벌(window) 객체로 설정한다.
  3. 만약 thisArg가 Object가 아니면 this를 ToObject(thisArg)로 설정한다.
  4. 위의 경우들이 아니라면 this를 thisArg로 설정한다.

: 전체적인 흐름을 보면 일단 strict모드가 아니므로 1 단계는 넘어가고, 2 단계에서 thisArg가 설정되지 않았을 경우 (undefined, null인 경우) window 글로벌 객체로 설정하는 것이 위의 예에서 #1과 #3, #5에 해당하는 경우이고, 이때에 [object Window]가 리턴된 것을 확인할 수 있다. 3 단계에서는 Object가 아닌 primitive type이 thisArg로 넘어온 경우 처리하는 단계로 아래와 같이 primitive type를 thisArg로 넘겨주면 그것을 Object로 바꿔서 잘 동작하는 것을 확인할 수 있다.

 

 

 

: 마지막 4 단계는 thisArg로 정상적인 Object가 넘어왔을 경우 this를 thisArg로 설정하는 것을 볼 수 있다. 이는 위의 예에서 #2, #4, #6의 경우에 해당하여 unikys 객체가 thisArg로 넘어와서 this로 설정된 것을 볼 수 있다. 그럼 이제 다시 윗단계로 올라가서 1 단계의 strict 모드에서의 동작을 알아보자.

 

 

* ECMAScript 5 strict mode

: strict 모드에 대해서는 따로 번외로 한번 다뤄야할 정도로 자바스크립트의 동작과 문법을 바꾸거나 제한하는 모드이다. 이것은 현재의 엄청 '자유로운' 자바스크립트에서 다소 '제한적인(strict)' 자바스크립트로 설정하는 것으로, 기존에는 없었던 syntax 에러들을 유발시키게 된다. 하지만 없던 에러가 생긴다는 것이 나쁜 것은 아니고, 자바스크립트가 너무 자유로워 실수, 오타 하나로 생긴 오류를 잡는 것이 엄청 힘들었던 것을 방지하고자하는 것이 바로 이 strict mode인 것이다. strict 모드에서 바뀐 문법의 가장 대표적인 예가 아래와 같다.

  • 변수는 무조건 선언하고 사용해야한다. (글로벌 변수 덮어씌우기 방지)
  • get과 set을 통하여 readonly등을 명확하게 설정하여 덮어씌우기 방지
  • 지울수 없는 속성을 지우는 경우 에러 발생 (Object.prototype 등)
  • 중복된 변수 선언 금지

: strict모드에서 들어가게 되는 경우 위와 같이 자바스크립트가 제한적이 되어서 보안이나 서로 다른 자바스크립트 라이브러리의 영역을 침범하는 경우를 개선하고자 한 것이다. strict mode를 사용하는 방법은 간단하다. 소스의 맨 위에 "use strict"; 를 추가하거나 strict 모드를 사용할 함수의 맨 위에 "use strict";를 추가하면 된다. 문자열이기 때문에 strict 모드를 지원하지 않은 브라우져에서는 그냥 넘어가게 되지만, strict 모드를 적용하게 되면 일부 동작들은 달라지게 될 것이므로, 적용을 하고자 한다면 이 둘의 차이에 대하여 정밀한 기능 테스트를 해야할 것이다.

 

: 그럼 strict 모드에 대해서는 일단 다음에 기회되면 자세하게 다루고, 위의 함수 호출의 내부 프로세스에서도 strict 모드가 영향을 1 단계에서 미치는 것을 확인할 수 있다. 그럼 한번 테스트해보자. 이번 테스트는 개발자 콘솔에서도 strict 모드가 잘 동작하는 파이어폭스에서 하였다.

 



 

: 위에서 whatsThis()를 호출하면 리턴되는 this가 undefined인 것이 기존의 [object Window] 였던 것과 바뀌게 된다. 이는 위의 내부 프로세스 1 단계에서 undefined가 오든, null이 오든 strict 모드인 경우 바로 this로 설정하는 것을 보면 이해할 수 있게 될 것이다. 이렇게 this를 제한하는 것은 함수의 잘못된 호출로 인하여 this를 이용해서 global의 데이터가 덮어씌워지거나 수정되는 것을 방지하고자 생긴 것이다. 만약 strict 모드로 한다면 this의 이해할 수 없는 동작도 조금은 방지할 수 있을지도 모른다.

 

: 그래도 strict 모드의 성급한 도입은 아직 모든 브라우져가 100% 지원하고 있지 않기 때문에 조심해야할 것이므로 이후에는 strict 모드에 대한 고려는 배제하도록 하자.

 

 

* arguments

: 이전 글에서 function에 대해서 다룰 때 이야기하려고 했던 arguments 키워드가 있다. 이것은 함수를 호출하게 되면 함수 내부에서 기본적으로 존재하게 되는 변수이고, 자바스크립트의 함수들이 기본적으로 인자의 수에 제한없이 유동적으로 활용가능한 것이 바로 arguments가 있기 때문이다. 이 arguments와 apply를 이용하면 위의 this에서 일어나는 혼란을 다소 억제시킬 수 있는 bind 함수를 만들 수 있다. 일단 arguments에 대해서 살펴보자.

function argTest() {
    return arguments;
}

argTest();
argTest("unikys", "tistory");

 

: 위의 결과는 아래와 같다.



 

: function argTest()에서는 인자를 하나도 선언을 안했지만, arguments라는 함수 내부의 변수를 통해서 인자로 넘어온 값을 순차적으로 접근할 수 있다. 위의 두 번째 경우에는 arguments[0] === "unikys", arguments[1] === "tistory"가 되어 접근이 가능하다. arguments가 생긴것은 length도 있고, 배열처럼 []로 index 기반 데이터 접근도 가능하여 Array 배열과 상당히 비슷하여 헷갈릴수도 있지만, Array와는 다른 객체이다. 아래의 예를 보자.

var args = argTest("unikys", "tistory");
args.length === 2;
args[0] === "unikys";
args[1] === "tistory";
args.slice(1); //   ? !== ["tistory"]

 

: 위와 같이 length도 있고, []로도 접근이 가능하지만 .slice(1) 함수를 호출하면 에러가 난다. arguments는 Array가 아니기 때문에 Array의 기본 내장함수가 없기 때문이다. 하지만, arguments가 생긴 것은 Array와 거의 비슷하기 때문에 Array의 함수를 가져와서 사용할 수도 있다. call과 apply을 이용해서 this를 수정하면 다른 객체의 함수가 마치 내 함수인 것처럼 사용할 수 있다. 따라서 argument도 Array의 함수가 마치 내 함수인것처럼 아래와 같이 가능하다.

 


: arguments 자체는 slice와 splice 함수가 없지만, call을 이용해서 Array의 기능을 똑같이 적용한 것을 확인할 수 있다. 이렇게 call과 apply를 통해 다른 객체의 함수를 마치 자신의 함수인 것처럼 사용을 할수도 있지만, 이는 내부 프로세스가 어떻게 돌아가는지 알고, 어떠한 영향을 미치는지 알고 있을 때 활용 가능할 것이다. 다음은 이 arguments와 apply를 조합하여 this를 고정하는 bind 함수를 구현해보자.

 

 

* this를 고정시키는 bind 함수

: 자바스크립트에서 this가 요동을 치는 가장 많은 경우는 바로 이벤트의 콜백함수로 사용할 때일 것이다.

var clickDiv1 = document.getElementById("clickDiv1");
clickDiv1.name = "world";

var unikys = {
    name: "unikys",
    say: function (e) {
        alert("Hello " + this.name);
    }
};

clickDiv1.addEventListener("click", unikys.say);
clickDiv1

 

: 위의 clickDiv1을 클릭해서 결과를 확인해보자. 위의 예에서 unikys.say() 함수는 this.name에 접근을 하고 있다. 객체지향 프로그래밍 언어에서는 당연히 unikys.name인 "unikys"가 출력될 것 같지만, 자바스크립트는 다르다. 위의 이벤트 리스너에 unikys.say를 넣게 되는 것은 또 다른 방법으로 표현한다면, 아래와 같이할 수도 있다.

 

var callback = unikys.say;
document.getElementById("clickDiv1").addEventListener("click", callback);

 

: 즉, 이벤트가 일어나서 콜백함수로는 함수가 하나의 변수로서 넘어가게 되고, 콜백이 호출 될 때에는 div를 컨텍스트로 호출이 되게 된다. 그렇다는 것은 콜백함수의 this는 해당하는 div 객체가 되기 때문에 "Hello world"가 뜨는 것이다. 특정 객체의 멤버 함수를 콜백함수로 넘겨줄 때에는 절대 생각지도 못한 오류일 것이다. 위처럼 멤버 함수의 this를 그대로 이용하고 싶다면 이전에 배웠던 간단한 임시 함수를 사용하거나 closure의 특성을 이용해서 콜백으로 부를 함수의 this를 고정시켜서 호출할 수가 있다.

var callback = function (e) {
    unikys.say();
};
document.getElementById("clickDiv2").addEventListener("click", callback);
clickDiv2

 

: 위의 clickDiv2를 클릭해보면 아주 간단한 방법으로 대체가 가능하다는 것을 알 수 있다. 하지만 이것만으로 끝나면 자바스크립트 개발자로서의 폼이 나질 않는다. 좀더 범용적이고 closure를 이용하는 '폼나는' 함수를 이용해보자.

 

function bind(obj, func) {
    return function () {
        func.apply(obj, arguments);
    };
}
document.getElementById("clickDiv3").addEventListener("click", bind(unikys, unikys.say));
clickDiv3

 

: 여기서 obj와 func는 closure scope에 남아있게 되고, 나중에 콜백 함수가 호출하게 되면, func.apply(obj, arguments);를 호출하게 된다. apply는 2번째 인자로 인자들의 배열을 받게 되는 것을 배열과 같은 구조인 arguments를 넘겨주게 된다. 이 bind 함수는 closure를 응용할 수 있는 아주 기본적이면서도 유용한 활용이다.

 

 

* 솔직한 고백

: 위의 bind 함수를 멋드러지게 구현을 했다! 하지만.. 솔직히 고백하자면 사실 bind는 이미 ECMAScript5 기반의 자바스크립트의 Function에 기본적으로 탑재 되어있다....

function yell() {
    alert("HELLO! " + this.name);
}
document.getElementById("clickDiv4").addEventListener("click", yell.bind(unikys));
clickDiv4

 

: 그렇다 괜히 bind로 고민하고 고생할 필요가 없이 만약 함수 내의 this가 항상 같은 this를 의도하고 있다면 bind를 사용하면 된다. 하지만 이렇게 직접 bind를 구현해봄으로써 this가 어떤식으로 돌아가는지 함수의 호출이 어떻게 돌아가는지 좀 더 속깊게 이해했으리라 믿는다. 직접 구현한 bind의 다른 의미를 찾자면, 함수의 this를 유동적으로 설정하고자할 때 사용하면 되는 함수로 남겨두면 될 것이다.

 

 

* 정리

- 각 함수의 호출 방법에 따라 this가 결정되는 방법은 아래 표와 같이 정리할 수 있다.

 호출 방식

this 

 func(args);

window 

"use strict"; 

func(args);

 undefined

 obj.func(args);

obj 

func.call(obj, args); 

obj (첫번째 인자) 

 func.apply(obj, args);

obj (첫번째 인자)

 

- arguments는 함수로 들어온 인자의 정보를 저장하고 있으며, Array와 비슷하지만 Array는 아니다.

- call이나 apply를 이용하면 다른 객체의 함수가 내것인양 사용할 수 있다.

- bind 함수를 이용하면 this를 고정시킬 수 있다. (직접 구현도 가능하고, ECMAScript5 이상에 기본 내장함수로 포함되어있다.

 

 

* 덤: call vs apply

: 사실 두개의 다른 점을 느끼기에는 많이 힘들 것이다. 하지만, call은 함수의 인자의 수가 항상 고정일 때 이용하면 좋고, apply는 위의 bind와 같이 인자의 수가 고정이 아니라 유동적일 때 배열에다가 인자를 다 넣어서 호출을 할 때 이용하면 편하다. 속도는 아무래도 객체를 하나 덜 생성해도 되는 call이 좋을거라 생각하지만, 체감은 별로 없을 것이다. 아래 사이트는 실제 테스트를 해본 결과이다. 파란색이 apply고 빨간색이 call이다. 수치가 높을 수록 성능이 좋은 것을 나타낸다.

 



 

: call과 apply 두개만 놓고 보면 call이 살짝 더 빠르기는 하지만 array를 생성하는 등의 상황을 따져본다면 아주 큰 차이는 없을 것이다. 실제로 테스트를 해보려면 아래 사이트에서 해보면 된다.

 

http://jsperf.com/test-call-vs-apply/3

 

 

 

* 다음에는 계속 살짝 맛보고 있는 closure에 대해서 자세하게 공부해보자.

 

함수를 호출하는 방법과 this의 이해 끝.

 

- 다음 편

2013/01/21 - [속깊은 자바스크립트 강좌] Closure의 이해 / 오버로딩 구현하기

2013/01/30 - [속깊은 자바스크립트 강좌] Closure 쉽게 이해하기/실용 예제 소스

2013/02/13 - [속깊은 자바스크립트 강좌] 쉬어가기: 웹 개발 방법론의 변화/자바스크립트의 재발견

2013/02/22 - [속깊은 자바스크립트 강좌] 객체지향의 기본: prototype

2013/10/04 - [속깊은 자바스크립트 강좌] 상속, new와 Object.create의 차이

2013/10/29 - [속깊은 자바스크립트 강좌] 글로벌(전역) 변수와 window 객체

2013/11/06 - [속깊은 자바스크립트 강좌] 변수 선언 방법에 대하여

2016/11/13 - [속깊은 자바스크립트 강좌] 마무리(는 책으로!)


공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
글 보관함