this 바인딩 이해하기

kyle kwon·2022년 10월 19일
1

JavaScript

목록 보기
3/5
post-thumbnail

Prologue

해당 내용은 블로그를 이전하면서 기존에 작성된 글을 옮겼습니다.
지난 closure 이해하기에 이어 this에 대해서 이해해 보겠습니다. this도 정말 자바스크립트에서 까다로운 존재라고 생각합니다. JS에서 함수를 호출하면 호출한 함수에 매개 변수를 인자로 넘겨줄 뿐만 아니라, arguments라는 객체와 this도 함께 전달하는데요.

이 중 this는 함수 호출 방식에 따라 다르게 바인딩 되는데, 이에 대해서 예시를 들면서 이해해 보겠습니다:)




this 바인딩?

자바스크립트는 함수 호출 방식에 의해 this에 바인딩할 특정한 객체가 동적으로 결정됩니다.
우리가 함수를 선언하는 단계에서 this에 바인딩할 객체가 결정되는 것이 아니라, 함수를 호출할 때 함수가 어떻게 호출되었는 지에 따라 this에 바인딩할 객체가 결정됩니다.

💡  쉽게 말하면, 함수를 잘못 컨트롤하면, 우리가 원하는 값을 얻을 수 없습니다.


◾️ 함수 호출 방식에 따른 this 바인딩 결과는 다음과 같이 분류 할 수 있습니다.

  1. 일반 함수 호출 시
  2. 객체 내부의 함수 → 메서드 호출 시
  3. 내부 함수 호출 시
  4. 클래스 내부 생성자 함수 호출 시
  5. 콜백 함수 호출 시

위의 분류들 중에서 내부 함수 호출 시 this가 바인딩 되는 대상을 예측하기가 제일 어렵습니다. 그러면, 다음과 같이 예시를 보면서, 이해해 보겠습니다.


01 일반 함수 호출 시 this

function foo () {
    console.log(this); // this === window(전역 객체)
}

foo()

→ 위와 같이 this는 전역 객체에 바인딩 됩니다. 일반 함수를 호출 하는 경우는 쉽죠?


02 메서드 호출 시 this

const person = {
    name: 'steven',
    age: 99,
    gender: 'Male',
    sayStack: function() {
        console.log(`${this.name} can use HTML, CSS, JS`);
    }
};

person.sayStack(); // steven can use HTML, CSS, JS



03 내부 함수 호출 시 this

let age = 1;
const animal = {
    name: 'tiger',
    age: 5,
    showAge: function() {
        console.log(`${this.name} is ${this.age} years old`);
        
        function plusAge() {
            this.age++;
            console.log(`in this func, ${this.name} is ${this.age} years old`);
        }
        plusAge();
    }
};

animal.showAge();
/*
tiger is 5
in this func,  is 2 years old
*/

위의 결과에서 보면, showAge 메서드의 함수 호출 시, 첫 출력에 this.name와 this.age는 각각  animal이라는 객체의 name과 age의 값을 참조해 5를 출력한 것을 볼 수 있습니다.

하지만, showAge의 내부 함수, plusAge를 호출 해 반환한 결과 값을 보면, this.name을 빈 String, this.age는 2를 반환하고 있습니다. 우리가 처음에 예상하기로는 this.age는 showAge에서 받은 5를 그대로 가져와, +1을 한 값을 반환할 것이라고 생각할 수 있습니다.



하지만, 우리가 예상한 방식과 다르게 작동합니다.

그 이유는 바로,
💡 내부 함수를 호출 했을 때 this는 전역 객체 window에 바인딩 되기 때문입니다.

즉, 외부 함수이자 animal이라는 객체의 메서드인 showAge 함수는 객체의 메서드로 호출된 것이기 때문에, this.name과 this.age는 animal이라는 객체에 바인딩 됩니다. 따라서, animal 객체의 name과 age라는 ‘값 프로퍼티’에 접근하여, ‘tiger’와 5라는 숫자를 출력합니다.

하지만, 내부 함수인 plusAge를 호출하면, 이 함수에서의 this는 window, 즉 전역 객체에 바인딩 되기 때문에, this는 전역 공간에 있는 age를 찾고, 그 age는 값을 1을 가리키고 있기 때문에, console로 출력 시 age가 + 1된 상태에서 2라는 값을 반환할 것입니다.



" 그럼 우리가 원래 예상한 대로 작동하도록 하려면 어떻게 해야 할까요? "

2가지 방법이 존재합니다.

01. 첫 번째 방법
다음과 같이 외부 함수 내에서 객체에 바인딩 된 this를 새로운 변수에 담아주고, 이 변수를 내부 함수에서는 새로운 '변수.프로퍼티'로 접근하여 출력하면 됩니다.

let age = 1;
const animal = {
    name: 'tiger',
    age: 5,
    showAge: function() {
        let that = this; // this === animal
        console.log(`${this.name} is ${this.age} years old`);
        
        function plusAge() {
            that.age++;
            console.log(`in this func, ${that.name} is ${that.age} years old`); // that === this
        }
        plusAge();
    }
};

animal.showAge();


02. 두 번째 방법

화살표 함수를 활용합니다.

💡 화살표 함수는 this가 원래 존재하지 않습니다.

원래 JS에서는 어떤 변수를 찾을 때, 현재 스코프 또는 환경에서 해당 변수가 없으면 상위 환경을 탐색합니다. 마치, 이전에 다뤘던 클로저와 유사하죠!
그 위의 환경에서도 해당 변수가 없을 경우에, 전역 공간까지 타고 올라가게 되는데, 없으면 그만두게 됩니다.
그래서 결국, 일반 함수에서 this 호출 시, 전역 공간인 window에 this가 바인딩 되었던 것이었죠!
따라서, 화살표 함수 호출 시 this라는 변수 자체가 존재하지 않기 때문에, 상위 환경에서의 this를 참조하게 됩니다.

let age = 1;
const animal = {
    name: 'tiger',
    age: 5,
    showAge: function() {
        console.log(`${this.name} is ${this.age} years old`);
        
        const plusAge = () => {
            this.age++;
            console.log(`in this func, ${this.name} is ${this.age} years old`);
        }

        plusAge();
    }
};

animal.showAge();

이렇게 내부 함수를 화살표 함수를 이용해서 선언하게 되면, 화살표 함수에는 원래 this라는 것이 존재 조차 하지 않기 때문에, 상위 환경으로 올라가서, this를 찾게 됩니다.

즉, showAge가 실행될 때의 this가 화살표 함수 내의 this가 되어, showAge 함수에서의 this는 animal이라는 객체의 프로퍼티의 값을 참조하기 때문에, 우리가 위에서 처음에 의도했던 결과(name은 tiger, age는 6)를 반환할 수 있습니다.

이해가 조금은 안될 수도 있지만,"우선은 화살표 함수는 this가 원래 존재하지 않고, this가 존재하지 않으므로, 그 this를 찾기 위해서 상위 스코프 또는 환경을 찾아 올라가는 것이다" 라고 생각하면 이해하기 쉬워지실 겁니다!




04 생성자 함수 호출 시 this

참고로, 생성자 함수를 호출하려면, 생성자 함수의 인스턴스를 만들어, 그 인스턴스를 통해 ‘값 프로퍼티’ 또는 메서드에 접근하면 해당 값을 반환할 수 있습니다.

function Car(name, price){
    this.name = name;
    this.price = price;
}

Car.prototype.sayPrice = function() {
    console.log(this.price);
}

let benz = new Car('benz', '10');
let hyundai = new Car('hyundai', '5');

console.log(benz); // Car { name: 'benz', price: '10' }
benz.sayPrice(); // 10

Car라는 생성자 함수는 name과 price라는 인자를 받는데, 이 값들은 this는 Car라는 생성자 함수에 바인딩 됩니다. 하지만, 생성자 함수를 호출하려면 위에서 얘기했다시피, new라는 키워드를 이용해서 인스턴스를 만들어 호출하기 때문에, 결국 해당 인스턴스에 바인딩 된다고 볼 수 있습니다.

또, 알다시피, 생성자 함수에서 메서드는 프로토타입을 이용해서 정의합니다. 위와 같이 해당 메서드가 this.price를 출력하도록 메서드를 정의했는데, 여기서 this는 Car라는 생성자 함수를 가리킵니다.
코드 하단에서 이 생성자 함수의 인스턴스, 바로 'benz'라는 인스턴스의 메서드인 sayPrice를 호출하면, this는 이 때 인스턴스인 benz를 가리켜, price라는 프로퍼티의 값 10을 반환할 것입니다.




05 콜백 함수 호출 시 this

const testTime = {
    timer: 50,
    setTimer: function (){
        setTimeout(function (){
            console.log(this.timer);
        }, 1000);
    }
}

testTime.setTimer(); // undefined

콜백 함수 호출 시에는 this는 전역 객체에 바인딩 됩니다. 따라서, 위의 코드 결과는 undefined가 나왔는데요.
위의 코드에서 this.timer의 this는 전역 객체 window를 가리키고 있고, 이 전역 객체에는 timer라는 변수가 존재하지 않기 때문에, 값이 없으므로 콜백 함수 호출 시에 위와 같이 undefined를 반환합니다.




Conclusion

이렇게 함수 호출 방식에 따른 this 바인딩에 대해서 알아봤습니다. 다음 포스트는 this를 변경해주는, 함수라는 일급 객체의 프로토타입이 갖고 있는 call, apply, bind 함수와 이벤트 캡쳐, 버블링, 위임 등에 대해서 다뤄 볼 예정입니다.

포스트를 읽어 보시다가, 수정되야 할 부분이 있다면 과감히 언급해주시면 감사하겠습니다. 🤔

profile
FrontEnd Developer - 현재 블로그를 kyledot.netlify.app으로 이전하였습니다.

0개의 댓글