this 그리고 apply, call, bind

30320 단어 ES5ES5

this

JavaScript의 함수 - 생성자 함수this라는 키워드가 굉장히 많이 나왔는데 이 this는 뭘 하는 녀석일까?🙄

this는 함수를 호출할 때 호출 방식에 따라 다르게 바인딩 되는 키워드이다.
백문이 불여일견, 코드부터 보면서 이해해보자.


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

func(); // node.js : global, browser : window

보시다시피, 일반적인 방법으로 함수를 호출하면 this전역 객체에 바인딩 된다.
그렇다면 어떤 경우에 this의 바인딩이 바뀌게 되는지 한 번 알아보자.





함수 호출 방식과 this


생성자 함수 호출

JavaScript의 함수 - 생성자 함수에서도 사용했던 예제를 다시 보자.


function Car(model, color){
    this.model = model;
    this.color = color;
}

var audi = new Car('S8L', 'black');
var genesis = new Car('G70', 'red');

console.log(audi); //Car { model: 'S8L', color: 'black' }
console.log(genesis); //Car { model: 'G70', color: 'red' }

new 연산자를 이용한 생성자 함수 호출 시 this생성자 함수를 이용해 생성한 객체에 바인딩 된다.
객체 지향 언어인 java의 생성자를 이용한 초기화에서도 볼 수 있던 익숙한 코드다.


객체의 메서드 호출

객체의 메서드를 호출 시 this해당 메서드를 호출한 객체에 바인딩 된다. 아래의 코드를 한번 보자.


var cat = {
    'sound' : 'meow',
    'playSound' : function(){
        console.log(this.sound);
    }
};

var dog = {
    'sound' : 'barkbark'
};

//dog 객체에 playSound 프로퍼티를 동적으로 생성하고, cat의 playSound 메서드를 참조
dog.playSound = cat.playSound;

cat.playSound(); // meow
dog.playSound(); // barkbark

cat객체의 playSound 메서드는 thissound 프로퍼티를 console에 출력해주는 메서드다.

dog객체에 동적으로 playSound 프로퍼티를 생성한 후, catplaySound 메서드를 참조한 결과
각 객체에 대해 바인딩 된 this가 서로 다른 것을 확인할 수 있다.


주의할 점

이러한 호출 방식에 따른 this 바인딩 때문에 가끔 원하지 않는 결과가 나올 때도 있다. 다음 예제를 한번 보자.


value = 0;

var obj = {
    'value' : 1, 
    'outerFunc' : function(){
        this.value += 1;
        console.log(this.value); // 2

        var innerFunc = function(){

            // innerFunc의 this는 전역객체에 바인딩 된다.
            this.value += 100;
            console.log(this.value); // 100
        };

        // outerFunc 메서드의 내부함수 innerFunc 호출
        innerFunc();
    }
}

// obj객체의 메서드 outerFunc 호출
obj.outerFunc();

의도했던 결과는 obj객체의 value가 102가 되게 만드는 것이었다.
하지만 결과는 obj객체의 value는 2가 되었고, 엉뚱한 전역객체의 value에 100이 더해졌다.😥

이것은 호출 방식에 따른 this 바인딩의 결과인데, 내부함수 역시 함수이므로 일반 함수 호출 시 this는 전역 객체에 바인딩 된다는 규칙에 따라 전역 객체에 100이 더해지게 된 것이다.

this를 별도의 변수에 저장해서 이런 의도하지 않은 작동을 방지하는 방법도 존재한다. 확인해보자.


value = 0;

var obj = {
    'value' : 1, 
    'outerFunc' : function(){
        var that = this;
        that.value += 1;
        console.log(that.value); // 2

        var innerFunc = function(){

            //innerFunc의 this는 더이상 전역객체에 바인딩되지 않는다.
            that.value += 100;
            console.log(that.value); // 102
        };

        // outerFunc 메서드의 내부함수 innerFunc 호출
        innerFunc();
    }
}

// obj객체의 메서드 outerFunc 호출
obj.outerFunc();

outerFunc 메서드의 내부에 that = this 구문을 추가해줬다.
이는 내부 함수의 상위 함수에 바인딩 된 this를 별도의 변수에 저장한 뒤, 해당 변수에 접근하는 방법이다.

물론 이 방식은 ES6에서 새로 생긴 Arrow Function의 등장으로 위의 방법은 권장되지 않지만 일단 알아두자.





명시적 this 바인딩

위의 주의할 점에서 봤듯, 함수 호출 방식에 따라 달라지는 this는 원하지 않는 결과를 만들 수도 있다. 이렇게 함수의 호출 방식에 따라 this 바인딩이 바뀌는 것이 아니라, 원하는 객체에 this를 바인딩하는 방법은 없을까?🤔

이 궁금증을 해결해 줄 메서드가 바로 Function.prototypeapply, call, bind 메서드다.

Function.prototype모든 함수의 부모 역할을 하는 프로토타입 객체이다. 따라서 모든 함수가 해당 메서드를 사용할 수 있다는 말이다. 그러면 이제 위의 메서드에 대해 살펴보도록 하자!😆


apply()

apply() 메서드의 매개변수는 다음과 같다.

func.apply(thisArg, [argsArray])

thisArg는 this로 바인딩 될 객체를 명시해주고, [argsArray]func함수 호출 시 전달 인자를 배열 형태로 전달한다.

어떻게 동작하는지 다음 코드를 통해 알아보자.


function printOwnProperty(){
    for(prop in this){
        console.log(prop + " : " + this[prop]);
    }
};

var Animal = {
    'name' : 'bbokbbok',
    'age' : 16,
    'sex' : 'Castrated Male',
    'breed' : 'Yorkshire Terrier',
    'species' : 'Canine'
};

printOwnProperty.apply(Animal); 
/* 
 * name : bbokbbok
 * age : 16
 * sex : Castrated Male
 * breed : Yorkshire Terrier
 * species : Canine
 */

(apply()는 어디까지나 해당 함수를 호출하는 것은 변하지 않고, this 바인딩만 바뀐다는 것을 이해하자.)

printOwnProperty()this로 바인딩 된 객체의 프로퍼티들을 모두 출력해주는 함수이다.

위에서 일반 함수 호출 시 this는 전역 객체에 바인딩 된다고 이미 알고 있다. 하지만 출력된 내용을 보면 printOwnProperty.apply()의 전달 인자로 전달한 Animal 객체의 프로퍼티를 출력해 주는 것을 볼 수 있는데, 이것이 apply()를 이용한 명시적 바인딩이다.

함수의 프로퍼티 - arguments에서 설명했던 유사 배열 객체는 배열 메서드를 사용할 수 없다고 설명했다.

하지만 apply()call() 메서드를 이용하면, 유사 배열 객체도 모든 배열 객체의 부모인 Array.prototype이 가지고 있는 다양한 배열 메서드의 사용을 가능하게 한다.


call()

call() 메서드의 매개변수는 다음과 같다.

func.call(thisArg[, arg1[, arg2[, ...]]])

apply()메서드와 큰 차이는 없고, 단지 전달 인자를 리스트의 형태로 받아온다는 것밖에 없다.

apply()call()의 차이를 아래 코드를 보며 알아보자.


var Person = function(name, gender, age){
    this.name = name;
    this.gender = gender;
    this.age = age;
  
    return this;
};

// 빈 객체
var emptyObj = {};

// { name: 'Johnson', gender: 'male', age: 15 }
console.log(Person.apply(emptyObj, ['Johnson', 'male', 15])); 

// { name: 'amanda', gender: 'female', age: 20 }
console.log(Person.call(emptyObj, 'amanda', 'female', 20));

이처럼 call()apply()전달 인자를 어떻게 넘겨주는가? 의 차이만 있다고 생각하자.

또한 apply()call()은 메서드 호출 시 해당 함수를 바로 호출한다. 그렇다면 bind()와의 차이는 뭘까?

bind()

bind() 메서드의 매개변수는 다음과 같다.

func.bind(thisArg[, arg1[, arg2[, ...]]])

위의 두 메서드와 bind() 메서드의 차이는, bind() 메서드는 호출 시 this를 첫 번째 전달 인자에 바인딩시킨 bound 함수를 반환한다.

역시 눈으로 보면 이해가 빠르다. 어떻게 사용하는지 확인해보자!😁


var Cat = function(sex, name){
    this.sex = sex;
    this.name = name;

    return this;
};

var emptyObj = {};

var bound = Cat.bind(emptyObj);

console.log(bound('Castrated Male', 'kitty')); // { sex: 'Castrated Male', name: 'kitty' }

bind() 메서드를 호출하면 Cat함수를 바로 호출하는 것이 아닌, bound라는 변수에 대입하고, bound를 이용해 다시 호출하는 것을 볼 수 있다.

그리고 아래처럼 bind()또한 call() 처럼 전달 인자를 리스트 형식으로 전달 할 수 있다.

var Cat = function(sex, name){
    this.sex = sex;
    this.name = name;

    return this;
};

var emptyObj = {};

var bound = Cat.bind(emptyObj, 'Castrated Male', 'kitty');

console.log(bound()); // { sex: 'Castrated Male', name: 'kitty' }

같은 결과가 나오는 것을 확인할 수 있다.

또, bind() 메서드는 전달 인자를 부분적으로 미리 지정하는 것도 가능하다. 아래를 참고하자.


var Cat = function(sex, name){
    this.sex = sex;
    this.name = name;

    return this;
};

var emptyObj = {};

var bound = Cat.bind(emptyObj, 'Castrated Male');

console.log(bound('kitty')); // { sex: 'Castrated Male', name: 'kitty' }
console.log(bound('nero')); // { sex: 'Castrated Male', name: 'nero' }
console.log(bound('kitty', 'nero')); // { sex: 'Castrated Male', name: 'kitty' }
console.log(bound()); // { sex: 'Castrated Male', name: undefined }

sex 프로퍼티를 미리 지정해놓고, name 프로퍼티만 따로 전달해주는 모습이다.

보시다시피 함수의 기본 특성처럼 초과된 전달 인자의 경우 무시하고, 부족한 전달 인자의 경우는 undefined로 처리되는 것을 알 수 있다.





참고 자료

MDN Web Docs - Function.prototype.apply()
MDN Web Docs - Function.prototype.call()
MDN Web Docs - Function.prototype.bind()

좋은 웹페이지 즐겨찾기