[JS] this

dk.han·2022년 10월 7일
0
post-thumbnail

This

컴파일 언어들과는 다르게 JavaScript, TypeScript는 동적으로 this 바인딩이 결정된다.(Dynamic binding). 때문에 원리를 제대로 모르면 오류를 발생시킨다고 하니, 공부하고 이해한대로 정리해보자.

Execution context (실행 환경)

Execution context는 실행 환경이라고 칭하는데, 단어 그대로 자바스크립트 코드가 실행되기 위한 환경에 대한 정보를 의미하며 총 3가지로 이루어져있다. 실행 컨텍스트는 전역에서 시작해, 함수가 호출될 때 스택에 쌓이게 된다.

  • this 포인터

    this가 가르키고 있는 참조 객체(reference variable)를 의미한다.
  • Variable object

    실행 컨텍스트( 자바스크립트가 실행되는 환경 )가 생성이 되면, 코드에서 참조하는 변수, 객체(함수 포함) 등의 정보들을 담은 객체를 생성하여 보관한다.
  • Scope chain

    스코프 체인은 일종의 리스트의 역할을 하며, 최상단 grobal 객체부터 실행 된 함수 스코프들의 레퍼런스를 차례대로 연결지은 것을 의미한다. 실행 컨텍스트가 생성되면 call stack에 쌓이게 되는데 이때, 이전 실행 컨텍스트들의 레퍼런스를 연결해놓은 고리라고 이해했다.
    해당 컨텍스트에 없는 변수를 찾아야 한다면 스코프 체인을 따라 올라가 찾게된다.

Execution context의 종류

  • 글로벌 실행 컨텍스트 (Global Execution Context)

    단 하나만 정의되는 전역 컨텍스트다. 전역 객체의 정보를 담고있으므로, this는 global object를 참조한다. 브라우저에서는 window, node에서는 module을 의미한다. Call stack에 가장 먼저 추가되고 앱 종료 시 삭제된다.

  • 함수 실행 컨텍스트(Functional Execution Context)

    함수가 호출될 때마다 새로운 실행 컨텍스트가 생성되어 스택에 쌓인다.

this가 가리키는 것

  • 함수 내부에서의 this는 stric 모드일 때와 아닐 때 다르다. stric 모드에서는 undefined, 아닐 땐 globalThis (전역 객체)를 가리킨다. 함수 스코프내에서 this에 대한 정보가 없기 때문

    function thisGame() {
      console.log(this);
    }
    thisGame(); // undefined or globalThis
  • 생성자 함수 or 클래스에서의 this는 앞으로 생성될 인스턴스 자체를 가리킨다.

    function Animal(name) {
      this.name = name;
      this.printName = function () {
        console.log(this.name);
      };
    }
    
    const dog = new Animal('까꿍이')
    dog.printName() // '까꿍이'

자바스크립트에서는 누가 호출하냐에 따라서 this가 달라진다. 즉, this를 호출하는 객체 (caller)에 의해 동적으로 결정이 된다. 아래 예시 코드를 참고해보자.

function Red(name) {
  this.name = name;
  this.printName = function () {
    console.log(` 빨강은 바로 ${this.name}`);
  };
}

function Blue(name) {
  this.name = name;
  this.printName = function () {
    console.log(` 파랑은 바로 ${this.name}`);
  };
}

const red = new Red('빨간색')
const blue = new Blue('파란색')
red.printName()  // 빨강은 바로 빨간색
blue.printName() // 파랑은 바로 파란색

// 이런식으로 호출하는 객체에 따라 this가 동적으로 바뀐다.
red.printName = blue.printName;
red.printName()  // 파랑은 바로 빨간색
blue.printName() // 파랑은 바로 파란색

// 이게 문제가 될 수 있는 이유??
function printColor(printName) {
  printName(); //obj.printName() 처럼 참조하는 객체가 없기 때문에 undefined
}

printColor(blue.printName); // undefined

이 처럼 동적 바인딩이 될 경우, 분명 blue.printName을 전달하여 함수를 실행했음에도 printColor 함수 내에서 printName()의 this는 전역 객체를 가르켜 undefined를 반환한다. 이럴 때 의도치 않은 에러가 발생할 수 있다.
그래서 이를 해결하기 위한 방법은 2가지가 있다.

  1. bind, call, apply 함수를 이용해서 수동적으로 바인딩 해주기.
    만들어지는 시점의 객체와 함수가 영원히 바인딩 된다. 하지만 함수를 만들때마다 bind를 써야하기 때문에 불편함이 있다.

    let o= {name: "Daniel",
            printName: function() {
              console.log("내 이름은",this.name);
            },
           };
    o.printName(); // 내 이름은 Daniel
    setTimeout(o.printName, 10);// 내 이름은 undefined
    setTimeout(o.printName.bind(o), 20); // 내 이름은 Daniel
  2. Arrow 함수를 이용해 정적바인딩을 해주는 방법.
    화살표 함수는 만들어지는 그 순간 렉시컬 환경에서의 this를 기억한다. 때문에 화살표 함수 밖에서 제일 근접한 scope의 this를 가리킨다. ( 호출된 함수를 둘러싼 실행 컨텍스트 )

    주의
    this를 공부하면서 가장 이해가 되지 않았던 부분이니 항상 주의하도록 하자.

    const obj = {
      name: 'dk',
      printName : () => {
        console.log(this)
      }
    }
    
    obj.printName() // 전역객체

    화살표 함수 밖에서 가장 근접한 scope의 this를 가르킨다고 배웠기 때문에, obj.printName()을 호출하면 당연히 this는 obj 객체를 반환할거라 예상하고 있었는데... 이게 뭐람?? 전역객체를 반환한다고??... 멘붕이었다.
    그래서 이유를 찾아보니 this는 클래스, 생성자함수, 함수 레벨에서만 존재하고, 그 외에는 상위 context의 상위 this를 참조 한다고 하였다. (엘리쌤 감사합니다.) 즉, 블록 레벨 스코프에서는 this가 존재하기 않기 때문에, 객체 리터널 내부에서의 this는 그 상위 this를 참조 한다는 것이다. 때문에 전역객체가 반환 될 수밖에 없었던 것!!
    이걸 알고나서 this라는 개념을 그래도 많이 이해하게 된것 같다.

화살표 함수의 특징

  • 문법이 깔끔하여 가독성이 좋다.
  • 생성자 함수로 사용이 불가능하다 ( 무거운 프로토타임을 만들지 않기 때문)
  • 함수 자체 arguments가 없다.
  • this에 대한 바인딩이 정적으로 결정된다.
    함수에서 제일 근접한 상위 스코프의 this에 정적으로 바인딩됨

Reference

0개의 댓글