본문 바로가기
Development

[21.01.08] JavaScript this 의 동작 원리

by igy95 2024. 1. 7.

스터디 시간 동안 this 에 대한 열띤 토론이 이루어졌지만, 무언가 명쾌하게 끝나지 못한 느낌이 들어 지금까지 듬성듬성 알아왔던 개념을 다시금 확립하고자 이 글을 작성하게 되었습니다. 혹시 글을 읽고나서도 이해가 잘 되질 않거나, 교정할 만한 내용이 있으시다면 피드백 부탁드리겠습니다 :)

References (Poiema Web)

  1. 실행 컨텍스트와 자바스크립트의 동작 원리
  2. 함수 호출 방식에 의해 결정되는 this
  3. 화살표 함수

위 링크에서 다루고 있는 주제를 순서대로 찬찬히 읽어 보신다면 아마 더 깊게 JS 의 동작원리를 이해할 수 있겠지만, 글의 성격에 맞게 몇몇 코드를 위주로 this 와 이를 이해하기 위해 필요한 약간의 배경 지식을 간단히 알아보겠습니다.

Execution Context (실행 컨텍스트)

ECMA 명세서에 따르면 Execution Context (이하 EC.)는 실행 가능한 코드를 형상화하고 구분하는 추상적 개념이라고 정의되어 있습니다. 일반적으로 JS 내에서는 전역 코드와 함수 코드로 이를 분류하고 있으며, 쉽게 말해 JS 엔진이 코드 실행에 필요한 정보(변수, 스코프 체인 etc.)들을 모으고 분류하기 위해 필요한 개념으로 유추할 수 있습니다.

 

EC 생성 순서

//전역 컨텍스트가 항상 일 순위로 생성된다.

var x = 'xxx';

function foo () {
  var y = 'yyy';

  function bar () {
    var z = 'zzz';
    console.log(x + y + z);
  }
  bar(); //(3)
}
foo(); //(2)

위의 예시 코드를 JS 엔진이 실행하기 시작한다면, EC 는 전역 ⇒ foo() ⇒ bar() 의 순서로 생성이 되고 이는 논리적 스택 구조를 이루게 됩니다. (자세한 글은 레퍼런스 링크 참조) 한마디로 후에 생성된 컨텍스트일수록 비교적 하위에 위치하며, 스택 구조로 판단하자면 위로 쌓일수록 하위에 위치하고 있습니다.

논리적 스택 구조

This (usual function vs arrow function)

JS 에서 this 는 그 자체만으로도 헷갈리지만, 일반 함수와 화살표 함수에서 차이점에서 더욱 더 혼란을 가중시킬 우려가 있습니다. 하지만 차근히 케이스들을 분리하고 바라 보면 아예 이해가 불가능한 개념은 아닙니다. 각 케이스를 면밀히 살펴보기 전에 함수의 종류에 따라 크게 나누어 보자면,

 

  • 일반 함수 안에서의 this동적으로 결정됩니다.
  • 반대로 화살표 함수 안에서의 this정적으로 결정됩니다.

이제, 그 예를 살펴보겠습니다.

usual function

선언식과 표현식으로 나타낼 수 있는 일반적인 함수에서의 this 는 해당 함수를 어떻게 호출하는 지에 따라 동적으로 결정됩니다. 일반적으로 함수의 호출 방식과 해당 방식에 따른 this는 다음과 같습니다.

 

  1. 함수 호출 ⇒ Global
  2. 메소드 호출 ⇒ 메소드를 호출한 주체
  3. 생성자 함수 호출 ⇒ 생성자 함수의 반환 값인 인스턴스 객체
  4. apply/call/bind 호출 ⇒ 해당 함수의 매개변수로 사용된 인자

여기서 중요한 부분은 아래 세 가지를 제외한 함수 호출 방식일 경우, 그 위치가 어디에 있든, 어떻게 쓰이든 간에 상관 없이 전역 객체를 this로 바인드하고 있다는 사실입니다. (브라우저: window / Node.js: global) 이는 함수가 내부에 위치하거나, 콜백 함수로 호출된다고 해도 전역에 묶여있다는 의미를 내포합니다.

arrow function

화살표 함수 같은 경우 this는 함수의 선언 단계에서 이미 정적으로 결정 되는데, 언제나 상위 컨텍스트(스코프)의 this를 가리킵니다. 여기서 살짝 헷갈릴 수 있는 부분이 참조하는 this 를 상위 스코프 그 자체로 오해하거나, 화살표 함수를 메소드로 사용하는 경우인 것 같습니다. 개인적으로 오늘 논의에 대한 대부분의 혼란이 여기서 출발 하지 않았나 싶습니다.

Code Review

퀴즈 4번의 세 코드를 다시 분석해 보자면,

const a = {
  age: '3',
  say: () => { //화살표 함수가 쓰였다? => 상위 컨텍스트의 this
    console.log(`내 나이는 ${this.age} 이다.`);
  }
}
a.say(); // 내 나이는 undefined 이다.

해당 코드에서 say()a 객체의 메소드로 사용되었고, 화살표 함수로 선언되었습니다. 이 코드에 대해 우리가 착각할 수 있는 부분은 눈으로만 판단한 블록 영역으로 인해, a 객체의 블록이 say() 블록의 상위 스코프가 아닌가 하는 의문일 수 있습니다. 하지만 a 는 메소드를 호출한 주체일 뿐, 메소드의 외부 스코프를 가지고 있는 것이 아닙니다!

 

화살표 함수가 그의 상위 컨텍스트 안의 this 를 참조한다면, 처음에 다루었던 JS 엔진이 컨텍스트를 호출하는 방식을 따라가 보면 어떨까요? 여기서는 컨텍스트가 호출 되었을 때 '열렸다'는 표현을 차용하겠습니다. (이해를 돕기 위함입니다.)

 

  1. Global EC 에서 a.say() 를 호출해 해당 컨텍스트가 열림.
  2. a.say() EC 에서 선언된 화살표 함수는 this 를 호출.
  3. this는 상위 컨텍스트인 Global EC 의 this 를 참조. ⇒ window() //브라우저 기준

그래서 this.agea 객체가 아닌 window 객체의 프로퍼티에 접근하여 undefined 라는 값을 얻었음을 알 수 있습니다.

두 번째 코드는 쉽게 가려낼 수 있습니다.

function Person(age) {
  this.age = age;
}
Person.prototype.myAge = function (a) {
  return a.map(function(x) {
    return `${x} ${this.age} 입니다.`;
  }; //map의 두 번째 인자가 있다면, 해당 주체가 콜백 함수의 this로 bind 된다.
}
const jeon = new Person(3);
const ary = jeon.myAge(['내나이는']);
console.log(ary); //['내나이는 undefined 입니다']

this 가 호출된 함수가 일반 함수 모양을 띄고 있고, 언급한 예외 케이스 세 가지 중 아무것도 포함 되지 않기 때문에 이 함수는 전역 컨텍스트의 this 를 참조합니다. 때문에 위의 코드와 동일하게 this.ageundefined 를 반환하게 됩니다. 이에 대한 해결책은 여러가지가 있는데 신기하게도 map() 자체에서 thisbind 해주는 기능이 있었습니다. 세 번째 코드는 역시 첫 번째 방식처럼 컨텍스트 생성 방향을 따라가보면 알 수 있습니다.

function Person(age) {
  this.age = age;
}
Person.prototype.myAge = function (a) {
  return a.map(x => `${x} ${this.age} 입니다`);
}
const jeon = new Person(3);
const ary = jeon.myAge(['내나이는']);
console.log(ary); //['내나이는 3 입니다']
  1. Global EC 에서 ary 에 결과 값을 할당하기 위해 jeon.myAge() 를 호출하고 해당 컨텍스트가 열림.
  2. jeon.myAge() EC 에서 a.map() 을 호출해 해당 컨텍스트가 열림.
  3. a.map() EC 에서 콜백으로 선언된 화살표 함수의 this 는 상위 컨텍스트의 this 를 참조.

동일한 과정을 통해 결국 참조하려는 thismyAge() 를 호출한 주체인 jeon 이라는 것을 알 수 있습니다.

만약 화살표 함수가 map() 의 콜백 함수로 바로 선언되지 않고 다른 곳에서 선언 되었다면, 동일한 함수를 사용했다고 하더라도 그 결과는 바뀔 수 있습니다.

var say = x => `${x} ${this.age} 입니다`; //this = window()

function Person(age) {
  this.age = age;
}
Person.prototype.myAge = function (a) {
  return a.map(x => say(x));
}
const jeon = new Person(3);
const ary = jeon.myAge(['내나이는']);
console.log(ary); //['내나이는 undefined 입니다']

this 가 참조한 값이 window() 라는 사실을 미루어 보아 화살표 함수의 this 는 호출 단계가 아닌 선언 단계 즈음에서 결정된다는 것을 짐작할 수 있습니다.

정리

  • JS 에서 this 는 일반적으로 해당 함수를 호출하는 방식에 따라 동적으로 결정됩니다.
  • 하지만 화살표 함수의 경우 해당 함수를 호출하면 선언 단계에서 정적으로 상위 컨텍스트를 참조하여 this 가 결정됩니다. (lexical this)

복습

  • 다음 코드의 this 는 무엇을 가르킬까요?
function foo(){
    function bar(){
        const sayThis = () => console.log(this);
        sayThis();
    }
    bar();
}

foo(); // ?
  • 답안
    window() - 전역 객체를 가리킵니다.
  1. Global EC ⇒ foo() EC ⇒ bar() EC ⇒ sayThis() EC
  2. 상위 EC인 bar() EC 의 this 참조
  3. bar()는 일반 함수이기 때문에 내부에 있어도 전역에 묶여 있다.