개발/Javascript

7. Javascript의 this와 execution context

JonghwanWon 2021. 7. 20. 17:25

this와 execution context

자바스크립트의 혼란스러운 개념 중 하나로 this키워드에 대해 알아보도록 하겠습니다.

더불어 실행 컨텍스트(execution context)도 간단히 알아보겠습니다.

실행 컨텍스트 또한 중요한 개념이기에, 다른 글에서 더욱 자세히 알아보겠습니다.

this와 실행 컨텍스트, 이 두가지 개념을 함께 이해하면 더이상 this가 혼란스럽지 않을 것 입니다!

 

들어가기 앞서 실행 컨텍스트에 대해 먼저 간단히 알아보겠습니다.

실행 컨텍스트(execution context)란?

자바스크립트 엔진은 코드를 실행하기 위해서 실행에 필요한 여러가지 정보(변수, 함수, 스코프, this)들을 알고 있어야 합니다.

실행에 필요한 정보를 형상화하고, 구분하기 위해 자바스크립트 엔진은 이들을 별도 객체로 모아 관리하게 됩니다.

 

즉, 실행 컨텍스트란 실행 가능한 코드가 실행되기 위해 필요한 환경 정보들을 모아놓은 객체라고 정의할 수 있습니다.

여기서 동일한 환경, 즉 하나의 실행 컨텍스트를 구성할 수 있는 방법은 전역공간, 함수 등이 있는데 흔히 실행 컨텍스트를 구성하는 방법인 함수를 실행하는 것에 초점을 두고 알아보겠습니다.

var x = 10;

function outer() {
    var y = 10;
    
    function inner() {
        var z = 10;
    }
    inner();
}

outer();

위 예제의 실행 과정을 보면 최초 자바스크립트 코드를 실행하는 순간 전역 컨텍스트가 생성 될 것이고(최상단의 공간은 코드 내부에서 별도의 실행 없이 브라우저에서 자동으로 실행되므로, 자바스크립트 파일이 열리는 순간 전역 컨텍스트가 활성된다고 이해하면 좋습니다), outer함수가 실행되며 outer에 대한 실행 컨텍스트가 생성되고, 최종적으로 outer내 inner함수가 실행되며 것 inner에 대한 실행 컨텍스트가 생성 될 것 입니다.

 

이때 생성 된 실행 컨텍스트들이 생성되며 저장되는 자료구조가 있는데 이를 콜 스택(call stack)이라 합니다.

실행 컨텍스트가 생성 될 때마다 콜 스택에 하나씩 쌓아올려 관련있는 코드들을 실행하게 되며 함수가 종료되면 해당 실행 컨텍스트를 제거하는 식으로 전체 코드의 환경과 순서를 보장하게 됩니다.

실행 컨텍스트와 콜 스택

이렇게 어떠한 실행 컨텍스트가 활성화 될 때 자바스크립트 엔진은 해당 컨텍스트에 관련 된 코드들을 실행하는 데 필요한 환경정보들을 수집해 실행 컨텍스트 객체에 저장하게 됩니다. 실행컨텍스트는 Variable Object(VO), Scope chain, thisValue 프로퍼티를 소유하게 됩니다.

thisValue 프로퍼티에는 this의 값이 할당되게 됩니다. 여기서 우리는 실행 컨택스트가 생성 될 때 this가 바라보는 대상이 결정되는 것을 알 수 있습니다.

 

this의 정의, 자바스크립트의 this란?

다른 대부분의 객체지향 언어에서 this는 클래스로 생성한 인스턴스 객체를 의미하며 클래서에서만 사용할 수 있는 반면에

자바스크립트의 this는 상황에따라 바라보는 대상이 달라지며, 어디서든 사용할 수 있습니다.

상황에 따라 달라지는 this

자바스크립트의 this가 바라보는 대상은 앞서 알아본 것 처럼 일반적으로 실행 컨텍스트가 생성 될 때 함께 결정됩니다.

즉, 함수가 어떤 방식으로 호출 되었는지에 따라 this가 바라보는 대상이 결정된다고 할 수 있습니다.

또한 지난 포스트에서 알아본 strict mode에 따라서도 일부 차이가 있습니다

2021.07.19 - [Javascript] - 6. Javascript의 strict mode

 

지금부터 다양한 상황과 각 상황별로 this가 어떤 값을 바라보게 되는지 살펴보겠습니다.

전역 실행 컨텍스트에서의 this

전역 실행 컨텍스트(global execution context)에서 this는 strict mode 여부에 상관없이 전역 객체를 참조합니다.

브라우저 환경이라면 this는 window객체를 바라보게 됩니다.

// 브라우저 환경에서
console.log(this); // Window { window: Window, ... }
console.log(this === window); // true

함수 실행 컨텍스트에서의 this

어떤 함수를 실행하는 방법은 여러가지가 있습니다. 그 중 가장 일반적인 두 가지(일반 함수호출, 메서드로써)에 대해 각각 알아보겠습니다.

함수 vs 메서드

함수와 메서드는 어떤 작업을 수행하기 위해 미리 정의한 코드블록이라는 점에서 동일합니다.
이 둘을 구분하는 유일한 차이는 독립성에 있습니다.
함수는 그 자체로 독립적인 기능을 수행하는 반면, 메서드는 자신을 호출 한 대상 객체에 관한 작업을 수행하는데 차이가 있습니다.
function func() {
    console.log(this);
}

var obj = {
    method: func,
}

// 함수로서 호출
func(); // Window

// 메서드로서 호출
obj.method(); // { method: f }
obj['method'](); // { method: f }

함수와 메서드의 차이를 아시겠나요? :)

 

메서드로써 호출시의 this

함수를 메서드로서 호출하게 되면 this의 값은 호출한 주체의 객체 정보를 바라보게 됩니다.

var obj = {
    x: 40,
    method: function() {
        return this.x;
    },
}

console.log(obj.method()); // 40

그렇다면 함수를 어떤 객체에 동적으로 할당한 후 호출하게 되면 어떻게 될까요?

function func() {
    return this.x;
}

var obj = {
    x: 40,
}

obj.method = func;

console.log(obj.method()); // 40

위 예제와 같이 같은 결과가 나오게 됩니다.

이는 어떠한 함수가 객체의 멤버로서 호출된 것만이 중요하다는 것을 보여줍니다.

즉, 함수를 실행할 때의 실행 컨텍스트와 연관지어 생각해보면 이해가 쉬울 것 입니다.

함수로서 호출시의 this

그렇다면 메서드와 달리 함수로서 호출될 때의 this가 바라보는 대상은 어떻게 결정될까요?

기본적으로 전역 객체를 참조하게 됩니다. 즉, 브라우저 환경에서는 window객체를 바라보게 됩니다.

하지만, 엄격 모드(strict mode)에서는 실행 컨텍스트가 생성 될 때의 값을 유지하므로 undefined로 남아있게 됩니다.

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

function func2() {
    'use strict';
    console.log(this);
}

func1(); // Window
func2(); // undefined

내부함수에서의 this

내부 함수의 경우 this는 어떤 대상을 가르키게 될까요?

앞서 알아본 것처럼 우리는 함수로서 호출 되었을때와, 메서드로서 호출될 때 this는 어떤 대상을 가르키는지 인지 알고 있습니다.

내부함수도 마찬가지로 적용됩니다.

var rootObj = {
  func: function () {
    console.log(this); // rootObj

    function inner() {
      console.log(this); // Window
    }
    inner();
  },
  obj: {
    func: function () {
      console.log(this); // obj
    },
  },
};

rootObj.func();
rootObj.obj.func();

위 예제와 같이 함수를 어떻게 호출 하는지에 따라 this가 가르키는 대상이 달라지게 되는 것이 중요합니다.

하지만 this라는 단어가 주는 인상과는 다른 것 같습니다.

this가 스코프 체인이 동작하는 것 처럼 현재 실행 컨텍스트에 바인딩 된 대상이 없으면 직전 컨텍스트의 this를 바라보게 할 수는 없을까요?

this를 우회하는 방법

ES6이전에서는 가장 대표적인 방법은 변수를 활용하는 것 입니다.

var rootObj = {
  func: function () {
    var self = this;
    console.log(this); // rootObj

    function inner() {
      console.log(self); // rootObj
    }
    inner();
  },
};

rootObj.func();

위 예제와 같이 상위 스코프에서의 this를 변수로 저장하여 활용하면 내부 함수에서 상위 스코프의 this를 사용 할 수 있습니다.

this를 바인딩하지 않는 함수

ES6에서는 함수 내부에서 this가 전역객체를 바라보는 문제를 보완하고자, 화살표 함수를 새로 도입했습니다.

화살표 함수는 실행 컨텍스트를 생성 할 때 this 바인딩 과정 자체가 빠지게 되어, 스코프체인 상 가장 가까운 this를 그대로 활용 할 수 있게 됩니다!

var rootObj = {
  func: function () {
    console.log(this); // rootObj

    var inner = () => {
      console.log(this); // rootObj
    };
    inner();
  },
};

rootObj.func();

위 방법 이외에 this를 명시적으로 바인딩할 수 있는 방법에 대해 알아보겠습니다.

bind, apply, call

자바스크립트는 this가 바라보는 대상을 명시적 지정할 수 있는 메서드(bind, call, apply)를 제공합니다.

참고
화살표 함수를  call, bind, apply를 사용해 호출할 때  this의 값을 정해주더라도 무시합니다. 사용할 매개변수를 정해주는 건 문제 없지만, 첫 번째 매개변수(thisArg)는 null을 지정해야 합니다.

bind

ES5에서는 Function.prototype.bind가 도입되었습니다.

bind 메소드가 호출되면 해당 함수와 동일하지만 this는 원본 함수를 가진 새로운 함수를 생성합니다.

새 함수는 this의 값이 영구적으로 첫번째 매개변수로 고정됩니다.

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

    const inner = innerFunction.bind(this);
    inner(); // obj { outer: f }
  },
};

obj.outer();

call과 apply

call과 apply 메소드는 첫번째 인수로 넘겨준 값을 this로 사용합니다.

함수의 문법은 apply와 거의 동일하지만, call은 thisArg와 인수들을 comma로 구분하여 순서대로 받는 반면에, apply는 thisArg와 인수들이 순서대로 담긴 배열(Array) 하나를 받는 것이 중요한 차이입니다.

call = comma, apply = array로 기억하시면 헷갈리지 않으실 것 입니다!

function add(c, d) {
  return this.a + this.b + c + d;
}

var obj = {
  a: 10,
  b: 5,
};

console.log(add.call(obj, 20, 30)); // 65
console.log(add.apply(obj, [20, 30])); // 65

생성자 함수로서 호출

new 키워드와 함께 호출된 경우의 this는 새로 만들어질 인스턴스(instance)가 됩니다.

생성자 함수를 호출하면, 생성자의 prototype 프로퍼티를 참조하는 __proto__라는 프로퍼티가 있는 객체(인스턴스, instance)를 만들고 미리 정의된 속성을 해당 객체(this)에 부여합니다.

이 과정을 거쳐 구체적인 인스턴스가 만들어지게 됩니다.

function Cat(name, age) {
  this.name = name;
  this.age = age;
}

var choco = new Cat("초코", 10);
var nabi = new Cat("나비", 8);

console.log(choco);
console.log(nabi);

 

'개발 > Javascript' 카테고리의 다른 글

9. JavaScript의 변수(var, let, const의 차이)  (0) 2022.06.09
8. Javascript의 콜 스택과 이벤트 루프  (0) 2022.03.29
6. Javascript의 strict mode  (0) 2021.07.19
5. Javascript의 스코프  (0) 2021.07.15
4. Javascript의 함수  (0) 2021.07.15