프로그래밍 언어/JavaScript

JavaScript | 실행 컨텍스트 (2) - This 바인딩

hyuga_ 2024. 3. 29. 16:24

앞서 실행 컨텍스트의 구성 요소 중 ThisBinding도 있다고 하였다. 이는 this 키워드가 무엇을 가리킬지를 나타내는 정보이다. 

 

코드를 짜다보면 지금 내가 호출하는 this가 무엇을 가리킬지가 머릿속에 그려져야 하는데, 자바스크립트의 this를 공부해보면 참 줏대가 없는 놈이라고 느낀다. 많은 사람들이 그럴 것이다. 

 

특정 함수가 호출될 때 실행 컨텍스트가 형성되는데, 이때 어떤 상황이냐에 따라서 this에 다른 대상이 바인딩된다. 그 패턴에 대해서 알아보자. 


선정리 후설명

  1. 전역 공간에서의 this
    • 전역 객체를 가리킨다. (브라우저 환경에서는 window 객체, Node.js 환경에서는 global 객체)
  2. 메서드 호출에서의 this
    • 메서드가 적용되는 객체를 가리킨다. (즉, 점(.) 앞에 붙어있는 객체)
  3. 함수 호출에서의 this
    1. 전역 객체를 가리킨다. strict mode라면 undefined를 반환한다. 함수 호출은 this를 지정하지 않기 때문이다. 
    2. 화살표 함수를 사용한다면, 렉시컬 this 바인딩이 적용되어, 해당 함수의 상위 컨텍스트의 this를 상속받는다.
      • 화살표 함수는 this 바인딩이라는 개념 자체가 적용되지 않는데, 이에 따라 자연스럽게 상위 컨텍스트의 this를 탐색하게 되는 것이다. 이를 렉시컬 this 바인딩이라고 이름 붙인다.
  4. 콜백함수에서의 this
    • 콜백함수는 언제나 함수 호출로 여겨지므로, 마찬가지로 전역 객체를 가리킨다. 
  5. 생성자에서의 this
    • new 키워드로 새롭게 인스턴스를 만들면, 그 안의 this는 새로운 인스턴스를 가리킨다.

 

상황별 this

전역 공간에서의 this

전역 공간에서 호출되는 this는 전역 객체를 가리킨다. 즉, 브라우저 환경에서는 window, Node.js 환경에서는 global 객체를 가리킨다. 

console.log(this === window); // true

 

메서드 호출에서의  this

호출된 메서드 내부의 this가 가리키는 대상은 점(.) 앞의 객체이다(메서드가 작용하는 객체).

const obj = {
  show() {
    console.log(this);
  }
};

obj.show(); // obj를 가리킴

 

 

[메서드 vs 함수]

흔히 메서드와 함수를 혼용해서 말하지만, 엄밀히 정의하면 다르다. 그 기준은 독립성에 있다. 함수는 그 자체로 독립적인 기능을 수행하는 반면, 메서드는 자신을 호출한 대상 객체에 관한 동작을 수행한다. 

특정 객체의 프로퍼티로서 함수를 정의하고, 이를 호출하면 무조건 메서드인게 아니다. 객체의 프로퍼티로서 호출해야 메서드이지, 그냥 함수로 호출한다면 이는 함수이다. 이를 확인하는 가장 직접적인 방법은 점(.) 의 유무 혹은 대괄호([])의 유무이다. 

 

 

함수 호출에서의 this

기본적으로 일반 함수 호출에서 this는 전역 객체를 가리킨다. 하지만, 엄격 모드('use strict';)에서는 this가 undefined가 된다.

그 이유는 실행 컨텍스트를 활성화할 당시에 this가 지정되지 않는 경우 this는 전역 객체를 바라보게 설계되어 있는데, 함수로서 호출될 경우 this가 지정되지 않기 때문이다. 

function show() {
  console.log(this);
}

show(); // 전역 객체, 만약 strict mode 라면 undefined

 

근데 이건 좀 부자연스럽다. 만약 함수로 호출되어 this가 지정되지 않았다면, 부모 스코프라든지 주변 환경의 this를 그대로 받으면 더 자연스럽지 않을까? 많은 사람들이 이렇게 생각하고, 이러한 부분이 JS 설계 상의 부족함이라고 자주 지적받는 포인트 중 하나이다.

 

그래서 이를 우회하는 방법들이 있다. 

 

1. this를 변수에 할당하기

외부 함수에서 this를 특정 변수에 바인딩하고, 해당 변수를 내부 함수에서 사용하는 방식이다. 

const obj = {
    outer: function () {
        console.log(this); // obj 객체에 대한 정보가 출력됨
        let innerFunc1 = function () {
            console.log(this); // window
        };
        innerFunc1();

        let self = this;
        let innerFunc2 = function () {
            console.log(self); // obj
        };
        innerFunc2();
    }
}

obj.outer();

 

 

2. 화살표 함수 사용하기 ⭐️

ES6에 도입된 화살표 함수를 사용하는 것이 좋은 대안이 될 수 있다.

 

화살표 함수는 this를 지정하지 않는 것을 넘어, this 바인딩이라는 개념 자체가 없다. 따라서, 화살표 함수 내부에서 this를 호출할 경우 상위 스코프의 this를 그대로 사용하게 된다. 상위 스코프, 즉 호출되었을 당시의 렉시컬 환경의 this를 그대로 상속받는다고 해서, '렉시컬 this 바인딩'이라고 부르기도 한다.

const obj = {
    outer: () => {
        console.log(this); // (1) window

        let innerFunc1 = function () {
            console.log(this); // (2) window
        };
        innerFunc1();
    }
}

obj.outer();

 

이 예제에서는 (1)과 (2) 모두 전역객체를 출력하지만 그 이유는 다르다. 

(1)의 경우 렉시컬 this 바인딩 때문이다. 화살표 함수를 사용했기 때문에 상위 컨텍스트인 전역 컨텍스트의 this를 상속받는다. 따라서 window를 반환한다.

(2)의 경우 함수 내부의 this 호출 때문이다. this가 함수로서 호출되었기 때문에 전역 객체를 반환한다. 따라서 window를 반환한다. 

 

 

콜백 함수에서의 this

콜백 함수 내부의 this는 언제나 전역 객체를 반환한다(strict mode 제외). 그 이유는 콜백 함수는 상위 함수가 메서드로 호출됐든 함수로 호출됐든 관계없이 언제나 함수로서 호출되기 때문이다. 

 

const obj2 = {
    name: 'kim',
    info() {
        setTimeout(function () {
            console.log(this.name); // undefined
        }, 1000)
    },
}

obj2.info(); // 메서드로서 호출했으나, 콘솔은 콜백함수 내에서 출력 중이다

 

 

여기서도 우회하는 방법을 하나 더 소개하면, bind() 함수를 통해 this를 명시적으로 전달하는 방식이 있다. 이를 명시적 this binding이라고 부른다. 명시적 this 바인딩은 위 함수에서 호출할 때도 사용가능하다. 이 방법 말고도 apply, call 을 활용한 방법이 있으나 이번에는 bind()만 다뤄보도록 한다. 

const obj3 = {
    name: 'young',
    info() {
        setTimeout(function () {
            console.log(this.name); // 'young'
        }.bind(this), 1000) // 호출하는 객체의 this를 인자로 건네주며, 똑같은 새로운 함수를 반환함.
    },
}

obj3.info();

 

 

 

생성자에서의 this

어떤 함수가 생성자 함수로서 호출된 경우, 그 내부에서의 this는 곧 새로 만들 인스턴스 자신이 된다. 즉 new 키워드로 생성자 함수를 호출하면, this는 새롭게 생성된 객체를 가리킨다.

 

function Person(name) {
  this.name = name;
}

const person = new Person("John");
console.log(person.name); // "John"