자바스크립트의 동작 원리를 이해하기 위해 실행 컨텍스트를 공부하던 중, this 바인딩에 대해 좀 더 자세히 이해해 보고자 작성하게 되었습니다.
this는 자신이 속한 객체 또는 자신이 생성할 인스턴스를 가리키는 자기 참조 변수(self-referencing variable)입니다. 클래스 기반 언어에서는 보통 클래스가 생성하는 인스턴스를 this로 가리킵니다. 프로토타입 기반 언어인 자바스크립트는 함수가 호출되는 방식에 따라 this에 바인딩 될 값을 결정합니다. 다시 말해 동적으로 this 바인딩이 결정됩니다.
💡 바인딩(binding)
식별자와 값을 연결하는 과정. 예를 들어, 변수 선언은 변수 이름(식별자)과 확보된 메모리 공간의 주소를 바인딩하는 것.
💡 this 바인딩(this binding)
this(키워드로 분류되지만 식별자 역할을 한다)와 this가 가리킬 객체를 바인딩하는 것.
다음 예제와 함께 소스코드의 처리 과정과 this 바인딩에 대해 살펴보겠습니다.
const name = "이름";
const gongyoo = {
name: "공지철",
getName: () => {
return this.name;
},
getRealName() {
},
};
console.log(gongyoo.name);
console.log(gongyoo.getName());
console.log(gongyoo.getRealName());
console.log(this.name);
function gildong() {
const name = "홍길동";
console.log(name);
console.log(this.name);
}
gildong();
가장 먼저 전역 객체를 생성합니다. 전역 객체는 전역 코드가 평가되기 이전에 생성됩니다. 따라서 전역 객체는 어떤 객체에도 속하지 않는 최상위 객체이며, 브라우저에선 window, Node.js에선 global 등 자바스크립트 환경에 따라 지칭하는 이름이 제각각입니다.
자바스크립트 엔진은 소스코드를 ‘평가’와 ‘실행’이라는 2가지의 과정으로 나누어 처리합니다.
전역 코드 평가 과정에서 전역 실행 컨텍스트를 생성하고 전역 환경 레코드와 외부 렉시컬 환경에 대한 참조로 구성된 전역 렉시컬 환경을 생성하여 전역 실행 컨텍스트에 바인딩합니다.
전역 환경 레코드는 객체 환경 레코드와 선언적 환경 레코드로 구성되어 있습니다. 객체 환경 레코드는 var로 선언한 전역 변수나 함수 선언문으로 정의한 전역 함수 등을 관리하고 선언적 환경 레코드는 let이나 const로 선언한 전역 변수를 관리합니다.
객체 환경 레코드는 BindingObject라고 부르는 객체와 연결되는데, 이는 앞서 생성된 전역 객체입니다. 전역 코드 평가 과정에서 var 키워드로 선언한 전역 변수나 함수 선언문으로 정의한 전역 함수를 이 BindingObject를 통해 전역 객체에 식별자로 등록합니다.
const 키워드로 선언한 name과 gongyoo를 선언적 환경 레코드에 식별자로 등록합니다. 등록한 식별자에 확보한 메모리 공간의 주소를 바인딩하고 빈 값을 할당합니다.
전역 환경 레코드의 [[GlobalThisValue]] 내부 슬롯에 this가 바인딩됩니다. 전역 코드에서 this는 전역 객체를 가리키고 전역 환경 레코드의 [[GlobalThisValue]] 내부 슬롯에는 전역 객체가 바인딩됩니다. 전역 코드에서 this를 참조하면 전역 환경 레코드의 [[GlobalThisValue]] 내부 슬롯에 바인딩되어 있는 객체가 반환됩니다.
현재 평가중인 소스코드는 가장 바깥 영역의 코드인 전역 코드이므로 외부 렉시컬 환경에 대한 참조에 null이 할당됩니다.
전역 코드의 평가가 끝나면 순차적으로 소스코드를 실행합니다. const 키워드로 선언한 name 변수에 ‘이름’이라는 값이 할당되고 const 키워드로 선언한 gongyoo 객체가 생성되고 할당됩니다.
console.log(gongyoo.name);
gongyoo 객체의 name 프로퍼티를 호출하였으므로 ‘공지철’을 출력합니다.
console.log(gongyoo.getName());
getName 메서드는 화살표 함수로 정의되어 있습니다.
🏹 화살표 함수는 자신만의 this 바인딩을 갖지 않습니다.
화살표 함수 내부에서 this를 참조하면 상위 스코프의 this를 그대로 참조하며 이를 lexical this라고 합니다. 이는 함수의 this가 함수가 정의된 위치에 의해 결정된다는 것을 의미합니다.
getName 메서드는 전역에서 정의되었기 때문에 this는 전역 객체를 참조합니다. 따라서 gongyoo 객체의 getName 메서드를 호출하면 화살표 함수 내부의 this가 전역 객체를 참조하기 때문에 ‘이름’을 출력합니다.
console.log(gongyoo.getName());
getRealName 메서드를 gongyoo 객체로 호출하였으므로 getRealName 메서드의 this는 gongyoo 객체를 참조합니다. 따라서 ‘공지철’을 출력합니다.
console.log(this.name);
this는 전역 객체를 참조하고 this로 name을 호출하였으므로 ‘이름’을 출력합니다.
gildong();
gildong 함수를 일반 함수로 호출합니다.
gildong 함수를 호출하면 전역 코드의 실행을 일시 중단하고 제어권이 gildong 함수로 넘어옵니다.
gildong 함수 코드 평가가 시작되고 gildong 함수 실행 컨텍스트를 생성합니다. 함수 환경 레코드와 외부 렉시컬 환경에 대한 참조로 구성된 gildong 함수 렉시컬 환경을 생성하고 gildong 함수 실행 컨텍스트에 바인딩합니다.
const 키워드로 선언한 name을 함수 환경 레코드에 식별자로 등록하고, 등록한 식별자에 확보한 메모리 공간의 주소를 바인딩하고 빈 값을 할당합니다.
gildong 함수는 전역에서 정의되었으므로 외부 렉시컬 환경에 대한 참조에 전역 렉시컬 환경을 할당합니다.
함수 환경 레코드의 [[ThisValue]] 내부 슬롯에 this가 바인딩됩니다. gildong 함수는 일반 함수로 호출되었으므로 this는 전역 객체를 가리킵니다. 따라서 함수 환경 레코드의 [[ThisValue]] 내부 슬롯에는 전역 객체가 바인딩되고, gildong 함수 내부에서 this를 참조하면 함수 환경 레코드의 [[ThisValue]] 내부 슬롯에 바인딩되어 있는 객체가 반환됩니다.
gildong 함수의 코드 평가가 끝나면 순차적으로 gildong 함수의 코드가 실행됩니다. const 키워드로 선언한 name 변수에 ‘홍길동’이라는 값이 할당됩니다.
console.log(name);
gildong 함수의 지역 변수 name을 호출하였으므로 ‘홍길동’을 출력합니다.
console.log(this.name);
gildong 함수 내부에서 this는 전역 객체를 참조하고 this로 name을 호출하였으므로 ‘이름’을 출력합니다.
gildong 함수에 더 이상 실행할 코드가 없으므로 gildong 함수의 실행 컨텍스트를 종료합니다.
전역 코드에 더 이상 실행할 코드가 없으므로 전역 실행 컨텍스트를 종료합니다.
앞서 ‘프로토타입 기반 언어인 자바스크립트에서는 함수가 호출되는 방식에 따라 this 바인딩이 동적으로 결정된다’고 설명했습니다.
이를 더 쉽게 이해해 보자면 호출되는 방식, 즉 호출하려는 객체가 무엇인지 알면 this가 가리키고자 하는 바를 알 수 있습니다. 변수나 함수를 호출하려는 객체가 전역 객체인지, const 키워드로 선언한 gongyoo인지 그 문맥(context)을 파악하면 this가 가리키는 값을 알 수 있습니다.
그리고 화살표 함수는 다른 함수들과 다르게 함수 자체의 this 바인딩을 갖지 않습니다. 따라서 화살표 함수 내부에서 this를 참조하면 일반적인 식별자처럼 스코프 체인을 통해 상위 스코프에서 this를 탐색합니다.
모던 자바스크립트 Deep Dive : 자바스크립트의 기본 개념과 동작 원리
자바스크립트는 왜 프로토타입을 선택했을까?