들어가기 전에

자바스크립트 개발자라면 알아야 할 33가지 개념 #15 자바스크립트 : this, call(), apply(), bind()

자바스크립트에서 "This" 이해하기

"this"의 개념적 개요

함수가 만들어졌을 때, 뒤에서는 this라 불리는 키워드가 만들어집니다. this는 함수가 동작하는 곳에 있는 오브젝트와 연결해줍니다.

this 키워드의 값은 그 함수 자체와는 상관이 없습니다. 함수가 어떻게 불려지는지가 this의 값을 결정합니다.

기본 "this" 컨텍스트

// define a function
var myFunction = function () {
  console.log(this);
};

// call it
myFunction();

this의 값은 어떤 것이 될지 예측할 수 있을까요? 기본 값으로, this는 언제나 전역 스코프의 root을 참조하는 window Object가 됩니다. 만일, 스크립트가 strict mode("use strict") 내에서 작동하고 있다면, this는 undefined일 것입니다.

오브젝트 리터럴(Object literals)

var myObject = {
  myMethod: function () {
    console.log(this);
  }
};

여기서 this의 값으론 무엇이 들어올까요?

  • this === myObject?
  • this === window?
  • this === anything else?

정답은 우린 모른다 입니다.

기억해두세요. this 키워드의 값은 언제나 함수 그 자체와는 상관 없습니다. 함수가 어떻게 호출되는지가 this의 값을 결정합니다.

코드를 조금만 바꿔봅시다.

var myMethod = function () {
  console.log(this);
};

var myObject = {
  myMethod: myMethod
};

이제 더욱 분명해질 겁니다.
물론, 여전히 this의 값은 우리가 함수를 어떻게 호출하냐에 따라 달렸습니다.

코드 안의 myObjectmyMethod라 불리는 프로퍼티를 갖습니다. 이 프로퍼티는 myMethod 함수를 가리킵니다. myMethod 함수가 글로벌 스코프로부터 호출됐을 때, this는 window object를 참조합니다. myObject의 메소드로서 호출됐을 때는, thismyObject를 참조합니다.

myObject.myMethod() // this === myObject
myMethod() // this === window

이러한 형식은 묵시적 바인딩(implicit binding) 이라 불립니다.

명시적 바인딩(Explicit binding)

함수에 명시적으로 컨텍스트를 바인딩할 때, 그것을 명시적 바인딩이라 합니다. 이러한 동작은 주로 call() 메소드와 apply() 메소드에 의해 이뤄집니다.

var myMethod = function () {
  console.log(this);
};

var myObject = {
  myMethod: myMethod
};

myMethod() // this === window
myMethod.call(myObject, args1, args2, ...) // this === myObject
myMethod.apply(myObject, [array of args]) // this === myObject

명시적 바인딩과 묵시적 바인딩 중에 어느 쪽이 더 우선순위를 가질까요?

var myMethod = function () 
  console.log(this);
};

var obj1 = {
  a: 2,
  myMethod: myMethod
};

var obj2 = {
  a: 3,
  myMethod: myMethod
};

obj1.myMethod(); // 2
obj2.myMethod(); // 3

obj1.myMethod.call( obj2 ); // ?????, 직접 해보세요.
obj2.myMethod.call( obj1 ); // ?????

명시적인 바인딩은 묵시적 바인딩보다 우위를 갖게 됩니다.

하드 바인딩(Hard binding)

하드 바인딩은 bind() (ES5)으로 가능합니다. bind() 메소드는 우리가 지정한 this 컨텍스트를 가진 기존 함수를 불러오기 위해 하드코딩된 새로운 함수를 반환합니다.

myMethod = myMethod.bind(myObject);

myMethod(); // this === myObject

하드바인딩은 명시적 바인딩보다 우위를 갖게 됩니다.

var myMethod = function () {
  console.log(this);
};

var obj1 = {
  a: 2
};

var obj2 = {
  a: 3
};

myMethod = myMethod.bind(obj1); // 2
myMethod.call( obj2 ); // 2 명시적 바인딩은 obj2이나, obj1로 하드바인딩 되어있음

'New' 바인딩(New binding)

function foo(a) {
  this.a = a;
}

var bar = new foo( 2 );
console.log( bar.a ); // 2

새로운 new 인스턴스를 참조하는 함수가 호출되었을 때, this가 만들어집니다.

역자 주: 위의 경우에는 변수 bar가 this가 되었습니다.

함수가 new와 함께 호출되었을 때는 묵시적, 명시적 또는 하드 바인딩을 신경쓰지 않습니다. 이 때는 그냥 새로운 인스턴스인 새로운 컨텍스트를 만들어냅니다.

function foo(something) {
  this.a = something;
}

var obj1 = {};

var bar = foo.bind( obj1 );
bar( 2 );
console.log( obj1.a ); // 2

var baz = new bar( 3 );
console.log( obj1.a ); // 2
console.log( baz.a ); // 3

역자 주: 위에서 bar 변수를 obj1로 바인딩 하였고, new 키워드 없이는 바인딩된대로 잘 동작하였으나, new 키워드가 붙은 이후에는 새로운 컨텍스트를 만들었습니다.

API 호출

때때로, 때때로 우리는 라이브러리나 헬퍼오브젝트를 사용합니다. (Ajax, event handling, etc.) 그리고 전달된 콜백을 호출합니다. 이러한 경우에는, this의 동작을 주의해야 합니다.

myObject = {
  myMethod: function () {
    helperObject.doSomethingCool('superCool',
      this.onSomethingCoolDone);
    },

    onSomethingCoolDone: function () {
      /// Only god knows what is "this" here
    }
};

코드를 보세요. 우리가 "this.onSomethingCoolDone"을 콜백으로 넘겼기 때문에, 스코프가 그 메소드를 참조하고 있다고 생각할 수도 있습니다.

이 부분을 고치기 위해, 몇가지 방법이 있습니다.

  • 주로 라이브러리들은 우리를 위해 또 다른 파라미터를 제공합니다. 우리는 그곳에 우리가 다시 가져오길 원하는 스코프를 전달할 수 있습니다.
myObject = {
  myMethod: function () {
    helperObject.doSomethingCool('superCool', this.onSomethingCoolDone, this);
  },

  onSomethingCoolDone: function () {
    /// Now everybody know that "this" === myObject
  }
};
  • 원하는 스코프를 하드 바인드 할 수도 있습니다.
myObject = {
  myMethod: function () {
    helperObject.doSomethingCool('superCool', this.onSomethingCoolDone.bind(this));
  },

  onSomethingCoolDone: function () {
    /// Now everybody know that "this" === myObject
  }
};
  • 클로져를 만들고 thisme에 캐시할 수도 있습니다.
myObject = {
  myMethod: function () {
    var me = this;

    helperObject.doSomethingCool('superCool', function () {
      /// Only god knows what is "this" here, but we have access to "me"
    });
  }
};

이 방법은 추천드리지 않습니다. 왜냐하면 메모리 누수를 초래할 수 있고, 진짜 스코프를 잊게 만들고 변수에 의존하게 만들기 때문입니다. 스코프가 정말 엉망이 되는 지경에 다다를 수도 있습니다.

This 문제는 이벤트 리스너, 타임아웃, forEach와 같은 것들에도 적용됩니다.

call(), apply(), bind() 이해하기

이 포스트에서 우리는 간단한 자바스크립트 코드 예제를 통해, call(), apply(), bind() 메소드의 차이에 대해 알아볼 것입니다. 자바스크립트에서 함수들은 오브젝트들이고, 위 3개의 메소드는 함수의 호출을 제어하기 위해 사용됩니다. call()apply()는 ECMAScript 3에서 소개되었고, bind() 메소드는 ECMAScript 5에서 추가되었습니다.

용례

함수를 즉시 호출하기 위해서 call() / apply() 메소드를 사용할 수 있습니다. bind() 는 함수가 나중에 실행됐을 때도 원본 함수를 호출할 때 갖는 올바른(correct) 컨텍스트(this)가 bind된 함수를 반환합니다. 그래서 bind()는 특정 이벤트에서 함수가 나중에 호출될 필요가 있을 때, 유용합니다.

call() 또는 Function.prototype.call()

아래 call() 메소드의 샘플코드를 확인해보세요.

//Demo with javascript .call()

var obj = {name:"Niladri"};

var greeting = function(a,b,c){
    return "welcome "+this.name+" to "+a+" "+b+" in "+c;
};

console.log(greeting.call(obj,"Newtown","KOLKATA","WB"));
// returns output as welcome Niladri to Newtown KOLKATA in WB

call() 메소드의 첫번째 파라미터는 함수가 호출되는 순간 "this" 오브젝트 값을 세팅합니다. 이 경우에는 "obj" 가 this 오브젝트였습니다.

나머지 파라미터들은 실제 함수의 인자들이었습니다.

apply() 또는 Function.prototype.apply()

아래 apply() 메소드의 샘플코드를 확인해보세요.

//Demo with javascript .apply()

var obj = {name:"Niladri"};

var greeting = function(a,b,c){
    return "welcome "+this.name+" to "+a+" "+b+" in "+c;
};

// array of arguments to the actual function
var args = ["Newtown","KOLKATA","WB"];  
console.log("Output using .apply() below ")
console.log(greeting.apply(obj,args));

/* The output will be 
  Output using .apply() below
 welcome Niladri to Newtown KOLKATA in WB */

call() 메소드와 비슷하게 동작합니다. 첫번째 파라미터는 함수가 호출되는 순간 "this" 의 값을 세팅합니다. 위의 경우에는 "obj" 오브젝트였습니다. apply() 메소드가 call() 메소드와 유일하게 다른 점은 두번째 파라미터에서 실제 함수의 인자 값을 배열로 받는다는 것입니다.

bind() 또는 Function.prototype.bind()

아래 bind() 코드의 샘플을 확인해보세요.

//Use .bind() javascript

var obj = {name:"Niladri"};

var greeting = function(a,b,c){
    return "welcome "+this.name+" to "+a+" "+b+" in "+c;
};

//creates a bound function that has same body and parameters 
var bound = greeting.bind(obj); 


console.dir(bound); ///returns a function

console.log("Output using .bind() below ");

console.log(bound("Newtown","KOLKATA","WB")); //call the bound function

/* the output will be 
Output using .bind() below
welcome Niladri to Newtown KOLKATA in WB */

bind() 메소드에 대한 위의 코드예제에서 우리는 context를 가진 나중에 호출될 bound 함수를 반환합니다. 우리는 bound 함수가 콘솔에서 다음과 같이 정의된 것을 볼 수 있습니다.

bound_func.jpg

bind() 메소드에 대한 첫번째 파라미터는 역시 bound 함수가 호출될 때, 타겟 함수에서 "this" 의 값을 세팅하는 부분입니다. bound 함수가 "new" 연산자를 이용하여 생성됐을때는, 바인드 시킨 this 값(첫번째 파라미터의 값)이 무시된다는 것을 알아두셔야 합니다. 나머지 파라미터들은 인자로 잘 넘겨집니다.

만일 당신이 초보자라면, 일단은 여기까지 알아두시면 될 것 같습니다. 읽어주셔서 감사합니다.