화살표 함수의 this

Taek·2021년 5월 22일
1
post-thumbnail

이전에 작성한 this에 대한 내용 중 화살표 함수 내부의 this에 대한 설명이 부족해 작성했습니다

const func = (function outer() {
    return () => console.log(this);
}.call({ x: 1 }));

class Foo {
    getThis() {
        console.log(this);
      	func();
      
      	const bindFunc = func.bind(this);
      	bindFunc();
    }
}

const foo = new Foo();
foo.getThis();

자바스크립트 엔진이 위 소스코드를 평가하고 실행하는 흐름을 생각해 보자


1. 전역 실행 컨텍스트가 생성되고 전역 렉시컬 환경이 구성된다.

소스코드는 평가와 실행 두 단계를 거치게 되는데 평가 단계에서 선언문이 먼저 실행 된다.

const func = (...);

class Foo {...}

const foo = ...;

위의 세 가지 선언의 식별자는 전역 렉시컬 환경을 구성하는 요소 중 Declarative Environment Record 영역에 수집된다. (var 키워드로 선언한 변수 또는 일반 함수 선언문은 Object Environment Record 영역의 BindingObject를 통해 global 객체의 프로퍼티 또는 메서드로 바인딩 됨)


2. 인터프리터에 의해 소스코드가 한 줄씩 실행 된다.

func = (...);		// (1) 소괄호 안의 즉시실행함수(outer) 실행 결과 반환
        
foo = new Foo();	// (2) 인스턴스 생성
foo.getThis();		// (3) 프로토타입 메서드 getThis 호출

편의상 전역 소스코드의 실행 순서를 간단히 표현했지만, 사실 (1)의 즉시실행함수가 실행 되면 소스코드의 흐름은 해당 함수(outer)로 넘어간다.


3. outer 함수 실행

func = (function outer() {
    // outer 함수는 값으로써 의미를 가지는 표현식이다.
    // 따라서 익명함수로 정의할 수 있지만 편의를 위해 outer라는 이름을 붙였다. 
    return () => console.log(this);
}.call({ x: 1 }));
3-1. 소스코드 평가

outer 함수 실행 컨텍스트가 생성되고 렉시컬 환경을 구성한다. 또한 outer 함수 정의에 대한 정보를 가진 객체인 함수 객체를 생성하는데 또 다른 내용이므로 이 글에서는 생략한다.

(함수 객체 맛보기 : 현재 실행 중인 소스코드의 렉시컬 환경을 outer 함수 객체 내부 슬롯 [[Environment]]에 저장해 추후 outer 함수가 실행 되었을 때 상위 스코프 참조(Outer Environment Reference)를 [[Environment]]에 저장된 값으로 바라보게 한다. 함수 객체에는 다양한 내부 슬롯과 내부 메서드가 존재한다.)

outer 함수 내부에는 선언문이 존재하지 않아 Environment Record에 수집되는 식별자는 없고, Outer Environment Reference에는 자신의 상위 렉시컬 환경(본문에서는 전역 렉시컬 환경)이 참조 된다.

this 바인딩을 살펴보면 outer 함수는 Function 생성자 함수의 프로토타입 메서드 call에 의해 호출되어 명시적인 this 바인딩이 되었다.


3-2. 소스코드 실행

렉시컬 환경 구성 후 소스코드 실행 단계에서 return문을 만나 정의된 함수를 평가(함수 객체 생성)한다. 해당 함수는 화살표 함수이며 화살표 함수는 다음과 같은 특징을 갖는다.

  1. constructor가 없다. (생성자 함수로 호출할 수 없음)
  2. prototype이 없다. (constructor와 prototype은 한 쌍)
  3. arguments가 없다. (rest parameter를 활용해야 한다.)
  4. this가 없다. (상위 스코프의 this를 참조)
  5. super가 없다. (상위 스코프의 super를 참조)

등등.. 화살표 함수는 function이 기본적으로 갖는 요소들이 제거되어 함수로써 역할에 집중할 수 있도록 제안된 사양인 것 같다. 때문에 성능적으로도 유리하다 생각한다.

이 글의 맥락에서 주목해야 할 특징은 화살표 함수에 this가 없으며 상위 스코프의 this를 참조 한다는 것인데, 본문의 소스코드 상에서는 outer가 상위 스코프이며 { x: 1 } 객체로 this 바인딩 되어있는 상태다.

따라서 return되는 화살표 함수의 this는 상위 스코프 outer의 this를 정적으로 기억한다. 이를 렉시컬 this라 한다.

일반적으로 this는 호출되는 지점에서 동적으로 정해지지만 하지만 화살표 함수에서는 정의된 위치에 의해 정적인 this를 갖게된다.
(개인적인 견해로 함수가 정의된 위치에서 렉시컬 스코프를 가지게 되는 것과 비슷한 개념인 것 같다.)


4. foo 인스턴스 생성 후 getThis 메서드 호출

foo = new Foo();
foo.getThis();
getThis() {
  console.log(this);	// (1) Foo {}
  func();		// (2) {x: 1}
  // func : () => console.log(this);
  
  const bindFunc = func.bind(this);
  bindFunc();		// (3) {x: 1}
}

Foo 생성자 함수의 프로토타입 메서드 getThis를 호출하면 위와 같은 결과가 출력 된다.
foo라는 인스턴스 객체에 의해 getThis가 메서드로 호출되었기 때문에

(1)에서는 해당 인스턴스 자신이 this로 출력되는 것을 확인 할 수 있고
(2)에서는 outer의 렉시컬 환경의 this로 바인딩된 값이 출력되는 것을 볼 수 있다. 함수가 평가될 때 함수 객체가 생성되어 렉시컬 스코프가 정해지는 것 처럼 렉시컬 this 역시 함수가 평가되는 순간 정해지는 것이기 때문에, (2)는 어디에서 호출하던 동일한 this의 내용이 출력된다.

foo.getThis.call({ y: 999 });

위와 같이 명시적으로 this를 지정해 호출하면 어떨까

getThis() {
  console.log(this);	// (1) {y: 999}
  func();		// (2) {x: 1}
  // func : () => console.log(this);
  
  const bindFunc = func.bind(this);
  bindFunc();		// (3) {x: 1}
}

(1)은 호출하는 방식에 따라 this가 동적으로 변하는 것을 볼 수 있지만
(2)는 정적으로 기억하고 있는 this를 출력한다.

func.call({ y: 999});	// {x: 1}
// func : () => console.log(this);

화살표 함수에는 this가 없지만 call, apply, bind 메서드로 명시적인 this를 지정해 호출해도 오류를 뿜진 않는다. 다만 this는 정적으로 바인딩 된 값을 출력한다.

0개의 댓글