ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • call과 apply, bind 메서드를 이용한 명시적인 this 바인딩
    개발 2020. 4. 20. 22:47

    이 전에 작성한 컨텐츠인 객체의 메서드를 호출할 때, 함수를 호출할 때, 생성자 함수를 호출할 때의 this 바인딩으로 각각의 상황에 따라 this가 정해진 객체에 자동으로 바인딩 된다는 것을 알아보았는데 자바스크립트는 이러한 내부적인 this 바인딩 이외에도 this를 특정 객체에 명시적으로 바인딩 시키는 방법도 제공합니다. 

     

    이를 가능하게 하는 것으로는 apply()call(), bind() 메서드 입니다.

    이 메서드들은 모두 함수의 부모 객체인 Function.prototype 객체의 메서드이므로 다음과 같은 형식으로 호출하는 것이 가능합니다.

     

    func.apply( thisArg,  argArr )

     

    여기서 기억해야 할 것은 apply() 메서드를 호출하는 주체가 함수고, apply() 메서드도 this를 특정 객체에 바인딩할 뿐 결국 본질적인 기능은 함수 호출 이라는 것입니다.

    만약 Person() 이라는 함수가 있다면 Person.apply() 이렇게 호출하고 이것의 기본적인 기능은 Person() 함수를 호출한다는 것입니다.

     

    그렇다면 그냥 호출하면 되지 왜 apply와 call 등을 사용하는지 이제부터 알아보도록 하겠습니다.

     

    1. apply() 메서드

    apply() 메서드에 대해 살펴보면 첫 번째 인자 thisArg는 apply() 메서드를 호출한 함수 내부에서 사용한 this에 바인딩할 객체를 가리킵니다. 즉, 첫 번째 인자로 넘긴 객체가 this로 명시적인 바인딩이 되는 것입니다. 

    두 번째 인자 argArr 인자는 함수를 호출할 때 넘길 인자들의 배열을 가리킵니다.

    apply() 메서드의 기능도 결국 함수를 호출하는 것이므로, 함수에 넘길 인자를 argArr 배열로 넘깁니다. 

     

    정리하자면 두 번째 인자인 argArr 배열을 자신을 호출한 함수의 인자로 사용하되, 이 함수 내부에서 사용된 this는 첫 번째 인자인 thisArg 객체로 바인딩 해서 함수를 호출하는 기능을 하는 것입니다.

     

    설명은 이정도로 해두고 코드를 통해 더 살펴보도록 하겠습니다.

    // 생성자 함수
    function Person(name, age){
      this.name = name;
      this.age = age;
    }
    
    // foo라는 빈 객체 생성
    let foo = {};
    
    // Person 생성자 함수를 apply를 통해 호출
    Person.apply(foo, ['foo', 25]);
    
    console.dir(foo);

     

    console.dir(foo); 의 출력은 다음과 같습니다.

     

    foo는 객체 리터럴 방식으로 생성한 빈 객체입니다.

    apply() 메서드를 사용해서, Person() 함수를 호출한 코드이고 첫 번째 인자로 넘긴 foo가 Person() 함수에서 this로 바인딩 됩니다.

    그리고 apply() 메서드의 두 번째 인자로 넘긴 배열 [ 'foo', 25 ]은 호출하려는 Person() 함수의 인자 name, age로 각각 전달됩니다.

    이 코드는 결국 Person( 'foo', 25 ); 함수를 호출하면서 this를 foo 객체에 명시적으로 바인딩하는 것을 의미하는 것입니다.

    console.dir(foo) 의 결과를 보면 foo 객체에 의도한 대로 프로퍼티가 생성되어 있음을 확인할 수 있습니다.

     

    이러한 apply() 메서드나 call() 메서드는 this를 명시적으로 원하는 값을 매핑하여 특정 함수나 메서드를 호출할 수 있다는 장점이 있습니다.

    arguments 객체와 같은 유사 배열 객체는 실제 배열이 아니므로 배열 메서드를 사용할 수 없는데 apply() 메서드를 이용하면 배열 메서드를 사용 할 수 있습니다.

     

    다음 예제는 func 함수에 전달된 인자에서 첫 번째 요소를 제외한 나머지 인자들을 가져오는 예제입니다.

    function func(){
      console.dir(arguments);
      
      // arguments 객체를 배열로 변환
      let args = Array.prototype.slice.apply(arguments, [1, 3]);
      console.dir(args);
    }
    
    func(1,2,3);

     

    배열에서는 slice(), shift() 메서드 등등을 통해 첫 번째 원소를 제외한 나머지를 바로 받을 수 있지만, arguments 객체는 유사 배열 객체이므로 slice()나 shift() 같은 배열 메서드를 사용하면 에러가 발생합니다.

    이러한 경우 apply() 메서드로 arguments 객체에서 마치 배열 메서드가 있는 것처럼 처리한 코드가 바로 Array.prototype.slice.apply() 입니다.

    이 코드를 해석하면 arguments 객체가 Array.prototype.slice() 메서드를 마치 자신의 메서드인 것처럼 arguments.slice() 와 같은 형태로 메서드를 호출하는데, 이 때 this는 arguments 객체로 바인딩 하고 있는 것입니다.

    두 번째 인자로 넘긴 [1, 3]은 apply() 메서드는 두 번째 인자로 배열을 받으니 배열을 써준 것이고 그 안에 원래 slice() 메서드에 인자로 들어가는 시작 인덱스와 종료 인덱스를 넣어준 것입니다.

     

    위 코드를 실행하여 console.dir()로 출력 된 결과는 다음과 같습니다.

     

    첫 번째 인자를 제외한 2와 3이 잘 출력 되는 것을 볼 수 있습니다.

     

    2. call() 메서드

    call() 메서드와 apply() 메서드의 동작은 본질적으로 같습니다.

    차이점은 함수에 인수를 넘기는 방법인데, apply() 메서드는 두 번째 인자에 배열로 넘겨주었고, call() 메서드는 두 번째 이후 인자부터 쉼표로 구분한 값의 목록을 나열합니다. 첫 번째 인자로 this 를 넘겨주는 것은 apply() 메서드와 동일합니다.

     

    이번 예제는 apply() 메서드와 call()메서드를 둘 다 적용해보고 출력되는 값을 보고 차이점을 알아보도록 하겠습니다.

    function say(greetings, ask){
      console.log(greetings + " " + this.name + " " + ask);
    }
    
    let foo = {name: 'hyojin'};
    
    say.apply(foo, ['Hello!', 'What are you doing?']);  // Hello! hyojin What are you doing?
    say.call(foo, 'Hi!', 'How old are you?');  // Hi! hyojin How old are you?

     

    두 번째 인자로 배열을 넘겨주어야할지와 두 번째 이후 인자부터 쉼표로 구별해서 넘겨주어야 할지의 차이점만 있을 뿐이고 출력 결과는 둘 다 동일한 것을 확인할 수 있습니다.

     

     

    3. bind() 메서드

    bind() 메서드가 call(), apply() 메서드와 다른점은 cal()l과 apply()메서드는 바로 함수를 호출하는데 bind() 메서드는 호출하지 않고 바인딩된 새로운 함수를 만듭니다. 즉, 객체에 함수를 묶는 것입니다.

    이 전 예제코드를 이용하여 bind()메서드에 대한 간단한 예제를 만들어 보겠습니다.

    function say(greetings, ask){
      console.log(greetings + " " + this.name + " " + ask);
    }
    
    let foo = {name: 'hyojin'};
    let bar = say.bind(foo);
    
    bar("Hello!", "Nice to meet you");  // Hello! hyojin Nice to meet you

    이 코드에서 bar 함수를 호출하면 항상 this가 foo 객체를 가리킵니다. 이처럼 say.bind(foo);는 bar 객체를 함수 say의 this로 설정한 새로운 함수를 만들어서 반환합니다.

     

    또 다른 예제로 타이머에 콜백함수를 넘겨주는 예제를 살펴보겠습니다.

     

    bind() 메서드를 사용하지 않았을 때

    function hello() {
      console.log(this.name);
    }
    
    let obj = {
      name: 'hyojin',
      hello: hello
    };
    
    setTimeout(obj.hello, 1000);  // 'global context'
    
    name = 'global context';

    setTimeout() 함수에 콜백함수로 obj.hello를 넘겨줬습니다. setTimeout() 입장에서는 obj 객체와는 전혀 상관없이 obj.hello가 가리키는 hello() 함수만 알고 있을 뿐입니다. 1초후에 실행하는 코드는 hello()함수를 기본 바인딩해서 실행하는 것과 동일합니다.

    따라서 위 결과는 ‘global context’가 출력되는 것입니다.

     

    bind() 메서드를 사용했을 때

    function hello() {
      console.log(this.name);
    }
    
    let obj = {
      name: 'hyojin',
      hello: hello
    };
    
    setTimeout(obj.hello.bind(obj), 1000);  // 'hyojin'
    
    name = 'global context!';

    obj.hello()를 콜백함수로 넘겨줄 때는 글로벌 컨택스트가 바인딩되는 반면, obj.hello.bind(obj)로 첫 번째 인자에 this 객체를 전달해 주니 의도한대로 hyojin이 잘 출력 된 것을 볼 수있습니다. 

    따라서, 1초 후에 hello() 함수가 실행될 것이고, 이때 this는 obj 객체를 가리키기 때문에 this.name 값은 ‘hyojin’이 되는 것입니다.

     

    ! 앞에 적힌 obj와 bind() 넘긴 obj객체는 항상 동일해야 합니다.
Designed by Tistory.