[JavaScript] This(1)- 상황에 따라 바뀌는 This

Donghwa Kim·2022년 12월 15일
0

JavaScript 동작원리

목록 보기
7/7

본 글은 정재남님의 <코어 자바스크립트>를 읽고 정리한 내용입니다.

들어가기 전에

생각해보면 지금껏 C#혹은 C언어로 코드를 작성하면서 this에 대해서 크게 고민해본적이 없는 것 같다. 보통 대부분의 객체지향 언어에서 this는 클래스로 생성된 개체 내에서 자기 자신을 가리킬 때만 사용하기 때문에 혼란의 여지가 없다.

하지만 자바스크립트에서 this는 어디서든 사용할 수 있고, 상황에 따라 바라보는 대상이 달라진다. 그렇기 때문에 이해하기에 혼란스럽고, 제대로된 동작을 이해하고 있지 못하다면 버그가 발생했을 때 그 원인을 찾기 매우 힘들어 질 것이다.

이번 포스팅에서는 이 괴랄하고 소문난 this에 대해서 깊게 이해해 보고, 자바스크립트에 대한 이해도를 한층 높여보고자 한다.

상황에 따라 달라지는 this

자바스크립트에서 this는 실행 컨텍스트가 생성될 때 함께 결정된다. 실행 컨텍스트는 함수가 호출될 때 생성되므로 함수를 호출할 때 생성된다고 말할 수 있다.

또한, 함수가 어떻게 생성되는지에 따라서 값이 달라진다.

지금부터 다양한 상황과 각 상황별로 this가 어떤 값을 바라보는지 알아보고 원인도 함께 알아보자.

전역 공간에서의 this

  • 전역 공간에서 this전역 객체를 가리킴

    • 개념상 전역 컨텍스트를 생성하는 주체가 바로 전역 객체이기 때문
    • 전역 객체는 자바스크립트 런타임 환경에 따라 다른 이름과 정보를 가짐
      • 브라우저 환경에서의 전역객체: window
      • Node.js 환경에서는 global
        // 전역 공간에서의 this (브라우저 환경)
        console.log(this); // {0: Window, window: Window, self: Window, document: document, name: '', location: Location, …}
        console.log(window); // {0: Window, window: Window, self: Window, document: document, name: '', location: Location, …}
        console.log(this === window); // true
  • 전역 변수를 선언하면 자바스크립트 엔진은 이를 전역객체의 프로퍼티로 할당한다

    var a = 1;
     console.log(a); // 1
     console.log(window.a); // 1
     console.log(this.a); // 1
    • 위 코드예제를 보면 전역공간에서 선언한 변수 a1을 할당했을 뿐인데 window.athis.a모두 1이 출력된다
    • 사용자가 var 연산자를 이용해 변수를 선언하더라도 실제 자바스크립트 엔진은 어떤 특정 객체의 프로퍼티로 인식하기 때문이다.
    • 여기서 특정 객체란 바로 실행 컨텍스트의 LexicalEnvironment(이하 L.E)이다.
    • 실행컨텍스트는 변수를 수집해서 L.E의 프로퍼티로 저장한다.
    • 이후 어떤 변수를 호출하면 L.E를 조회해서 일치하는 프로퍼티가 있을 경우 그 값을 반환한다.
    • 전역 컨텍스트의 경우, L.E는 전역객체를 그대로 참조한다.

메서드로 호출할 때 그 메서드 내부에서의 this

함수 vs 메서드

  • 어떤 함수를 실행하는 가장 일반적인 방법 두 가지함수로서 호출하는 경우메서드로서 호출하는 경우이다.

  • 프로그래밍 언어에서 함수와 메서드의 차이는 독립성에 있다.

    • 함수는 그 자체로 독립적인 기능을 수행하지만,
    • 메서드는 자신을 호출한 대상 객체에 관한 동작을 수행한다
    • 자바스크립트는 상황별로 this 키워드에 다른 값을 부여하게 함으로써 이를 구현했다
    • 자바스크립트에서 메서드의 의미는 다른 객체지향 언어와 조금 다르다
      - 어떤 함수를 객체의 프로퍼티에 할당한다고 해서 그 자체로서 무조건 메서드가 되는 것이 아니라,
      - 객체의 메서드로서 호출할 경우에만 메서드로 동작하고, 그렇지 않으면 함수로 동작한다


아래 코드를 통해 살펴보자

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

// func(1); // global 1

var obj = {
  method: func
};

obj.method(2); // obj  2

익명 함수 func는 그대로 이지만 함수로서 호출한 경우와, obj 객체의 프로퍼티에 할당해서 호출 (메서드로서 호출)한 경우 this가 달라지는 것을 볼 수 있다.

var obj = {
  method: function (x) { console.log(this, x) }
}

obj.method(1); // { method: [Function: method] } 1
obj['method'](2); // { method: [Function: method] } 1

보통 함수로서의 호출과 메서드로서의 호출을 구분할 때, 함수 앞에 점(.)이 있으면 메서드로서의 호출, 없으면 함수로서의 호출이라 하는데,

위 코드 예제 처럼 대괄호 표기법으로 작성하여도 메서드로서 호출한 것이 된다. 어떤 함수를 호출할 때 그 함수 앞에 객체가 명시되어 있는 경우에는 메서드로 호출한 것이고 그렇지 않은 모든 경우에는 함수로 호출한 것이다.

메서드 내부에서 this

this에는 호출한 주체에 대한 정보가 담긴다. 어떤 함수를 메서드로 호출하는 경우 호출 주체는 함수명(프로퍼티명) 앞의 객체이다. 점 표기법의 경우 마지막 점 앞에 명시된 객체가 곧 this가 된다.

var obj = {
  methodA: function () { console.log(this); },
  inner: {
    methodB: function () { console.log(this); }
  }
}

obj.methodA(); // obj
obj['methodA'](); // obj

obj.inner.methodB(); // inner
obj.inner['methodB'](); // inner

obj['inner'].methodB(); // inner
obj['inner']['methodB'](); // inner

함수로서 호출할 때 그 함수 내부에서의 this

함수 내부에서의 this

어떤 함수를 함수로서 호출할 경우에는 this가 지정되지 않는다. 실행 컨텍스트를 활성화할 당시에 this가 지정되지 않은 경우 this는 전역객체를 바라본다. 따라서 함수에서의 this는 전역 객체를 가리킨다.

더글라스 크록포드(Douglas Crockford)는 이를 명백한 설계상의 오류라고 지적한다. 바로 밑에서 그 이유에 대해서 알아보자.

메서드 내부함수에서의 this

메서드 내부에서 정의하고 실행한 함수에서의 this가 자바스크립트 초심자들이 가장 혼란을 느끼는 지점 중 하나라고 한다.

아래 예제의 출력값을 예상해 보자

var obj1 = {
  outer: function () {
    console.log(this); //  -----------(1)
    var innerFunc = function() {
      console.log(this); //  -------(2)
    }

    innerFunc();

    var obj2 = {
      innerMethod: innerFunc //  ------(3)
    };

    obj2.innerMethod();
  }
}

obj1.outer();

출력값은 (1) obj1, (2) global, (3) obj2가 나온다. 필자의 경우 답을 보기 전에 (2)의 정답을 obj1으로 예상했다.

하지만 앞서 설명한 규칙, 함수로서 호출이냐 메서드로서 호출이냐의 원칙만 알고 있으면 this가 무엇을 가리키는지 명확하게 알 수 있을 것이다.

(2)의 경우도 단순히 함수로서 호출이 되었기 때문에 전역객체 global이 바인딩 된 것이다.

즉, this 바인딩에 관해서는 함수를 실행하는 당시의 주변 환경 (메서드 내부인지, 함수 내부인지 등)은 중요하지 않고, 오직 해당 함수를 호출하는 구문 앞에 점 또는 대괄호 표기가 있는지 없는지가 관건이다.


메서드의 내부 함수에서 this를 우회하는 방법

그럼 호출 주체가 없을 때 자동으로 전역객체를 바인딩하지 않고 호출 당시 주변 환경의 this를 그대로 상속받아 사용하는 방법은 없을까?

그게 훨씬 자연스러울 뿐더러 자바스크립트 설계상 이렇게 동작하는 편이 스코프체인과의 일관성을 지키는 설득력 있는 방식이다. 변수를 찾을 때 우선 가장 가까운 L.E를 찾고 없으면 상위 스코프를 찾는 것 처럼, this 역시 현재 컨텍스트에 바인딩된 대상이 없으면 직전 컨텍스트의 this를 바라보도록 말이다.

ES5까지는 자체적으로 내부함수에 this를 상속할 방법이 없다.
하지만 변수를 활용하면 아래와 같이 내부함수에 this를 상속할 수 있다.

var obj = {
  outer: function () {
    console.log(this); // obj

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

    innerFunc1(); // global

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

    innerFunc2();
  }
}

obj.outer();


this를 바인딩하지 않는 함수

ES6에서는 함수 내부에서 this가 전역객체를 바라보는 문제를 보완하고자, this를 바인딩하지 않는 화살표 함수(arrow function)을 새로 도입했다. 화살표 함수는 실행 컨텍스트 생성시 this 바인딩 과정 자체가 빠지게 되어, 상위 스코프의 this를 그대로 활용할 수 있다. 그러면 직전에 살펴보았던 우회법은 불필요해진다.

var obj = {
  outer: function () {
    console.log(this); // obj

    var innerFunc = () => {
      console.log(this); // obj
    }

    innerFunc();
  }
}

obj.outer();

이 외에도 call, apply 등의 메서드를 활용해 함수를 호출할 때 명시적으로 this를 지정하는 방법이 있다. 이건 다음 포스팅에서 다루어보겠다.

콜백 함수 호출 시 그 함수 내부에서의 this

함수 A의 제어권을 다른 함수(또는 메서드) B에게 넘겨주는 경우 함수 A를 콜백함수라고 한다. 이 때 함수 A는 함수 B의 내부 로직에 따라 실행되며, this역시 함수 B 내부로직에서 정한 규칙에 따라 값이 결정된다. 콜백 함수도 결국은 함수기 때문에 this가 전역객체를 참조하지만, 제어권을 받은 함수에서 콜백함수에 별도로 this가 될 대상을 지정한 경우에는 그 대상을 참조하게 된다.

setTimeout(function () { console.log(this) }, 300); // global

[1, 2, 3, 4, 5].forEach(function (x) {
  console.log(this, x); // global, 1 global, 2 ... global, 5
});

위 예제에서 setTimeout 함수와 forEach 메서드는 그 내부에서 콜백함수를 호출할 때 대상이 될 this를 따로 지정하지 않았기 때문에 모두 global을 출력하는 경우이다.

생성자 함수 내부에서의 this

자바스크립트는 함수에 생성자로서의 역할을 함께 부여했다. new 명령어와 함께 함수를 호출하면 해당 함수가 생성자로서 동작하게 된다. 그리고 어떤 함수가 생성자 함수로서 호출된 경우 내부에서의 this는 곧 새로 만들 구체적인 인스턴스 자신이 된다.

생성자 함수를 호출하면 우선 생성자의 prototype 프로퍼티를 참조하는 __proto__ 라는 프로퍼티가 있는 객체를 만들고, 미리 준비된 공통 속성 및 개성을 해당 객체(this)에 부여한다. 이렇게 구체적인 인스턴트가 만들어진다.

var Cat = function (name, age) {
  this.bark = '야옹';
  this.name = name;
  this.age = age;
}

var choco = new Cat('초코', 10); // (1)
var navi = new Cat('나비', 7); // (2)

console.log(choco); //  (3) Cat { bark: '야옹', name: '초코', age: 10 }
console.log(navi); // (4) Cat { bark: '야옹', name: '나비', age: 7 }

위 예제 코드의 출력결과 (3), (4)를 보면 choconavi가 Cat 클래스의 인스턴스 객체로서 생성된 것을 볼 수 있고, (1)과 (2)에서 실행한 생성자 내부함수에서의 this는 자기자신 (각각 choconavi) 을 가리킴을 알 수 있다.

정리

  • 전역공간에서의 this는 전역객체 (브라우저에서는 window, Node.js에서는 global)을 참조한다.

  • 어떤 함수를 메서드로서 호출한 경우 this는 메서드 호출 주체 (메서드 앞의 객체)를 참조한다.

  • 어떤 함수를 함수로서 호출한 경우 this는 전역 객체를 참조한다. 메서드의 내부 함수에서도 같다.

  • 콜백 함수 내부에서의 this는 해당 콜백 함수의 제어권을 넘겨받은 함수가 정의한 바에 따르며, 정의하지 않은 경우에는 전역객체를 참조한다.

  • 생성자 함수에서의 this는 생성될 인스턴스를 참조한다.

profile
Slow but steady wins the race🏃‍♂️

0개의 댓글