자바스크립트의
this
는 상황에 따라 값이 다르기 때문에, 현재 상황에서 어떤 값을 가지고 있는지 매우 헷갈린다. 오늘도this
를 순간 착각하고 오해한 일이 있어서 이 기회에this
에 대해 한번 정리해보려고 한다.
책모던 자바스크립트 딥다이브
의 내용을 참고했습니다.
this
는 왜 필요할까? 객체는 프로퍼티
와 메서드
로 이루어져 있다. 프로퍼티
는 객체의 상태를 나타내고, 메서드
는 객체의 동작을 나타내므로, 메서드
가 프로퍼티
값에 접근하지 않으면 일반함수
와 다를게 없다. 그 상황에서 객체는 네임스페이스의 역할밖에 하지않는다. 그렇기 때문에 메서드
가 프로퍼티
혹은 다른 메서드
에 접근하기 위해서는 해당 객체를 가리키는 식별자
를 참조할 수 있어야 한다.
const obj = {
id: 1,
getId() {
console.log(id)
}
}
obj.getId(); // ReferenceError: id is not defined
위 코드는 왜 id
값을 참조하지 못하고 에러를 낼까? 현재 getId()
메서드 내부의 console.log(id)
의 인자로 준 id
는 식별자
다. 그런데 객체 내부의 id
는 식별자
가 아니라 프로퍼티
다. 없는 식별자
를 참조하려고 했기 때문에 reference error
가 났다.
이런 경우, id
라는 다른 식별자
가 있다면 참조할 수도 있다.
const id = 2;
const obj = {
id: 1,
getId() {
console.log(id);
}
}
obj.getId(); // 2
기존의 프로퍼티
의 값에 접근하기 위해서는 아래와 같이 프로퍼티
앞에 객체가 오고 뒤에 마침표.
나 대괄호[]
가 와야 한다.
const obj = {
id: 1,
getId() {
console.log(obj.id);
console.log(obj['id']);
}
}
obj.getId();
// 1
// 1
getId()
내부 코드가 평가되는 시점은 obj.getId()
가 호출될 때이다. obj.getId()
가 호출되기 이전에 객체 리터럴은 평가되어 obj
의 값으로 할당되어 있기 때문에 내부에서 obj
를 참조할 수 있다. 위의 코드의 상황에서는 id
의 값을 참조할 수 있기에 부족한 점이 없어보인다.
const obj = {
id: 1,
getId() {
console.log(obj.id);
}
}
obj.getId(); // 1
const obj2 = {
id: 2,
getId: obj.getId,
}
obj2.getId(); // 1
그러나 메서드
를 호출한 객체
가 메서드
를 정의한 객체
라는 보장은 없다. obj2
의 getId
는 obj1
의 getId
를 참조하고 있다. 바람직한 코드는 아니지만, 메서드
는 함수
이기 때문에 다른 메서드
나 변수
의 값
으로 할당할 수 있다. obj2.getId()
는 obj2
의 id
값을 출력할거라고 기대하고 사용했지만 getId
는 obj
의 id
를 참조하고 있기 때문에 똑같이 1
이 출력된다. 그렇기 때문에 메서드
내부에서는 참조하고자 하는 프로퍼티
나 메서드
를 가지고 있는 객체
를 참조할 수 있는 별도의 식별자
가 필요하다.
const obj = {
id: 1,
getId() {
console.log(this.id);
}
}
obj.getId(); // 1
const obj2 = {
id: 2,
getId: obj.getId,
}
obj2.getId(); // 2
해당 객체
를 참조하는 식별자
의 역할을 해주는 것이 this
다. this
는 객체
자기 자신을 가리키므로, 위 코드에서 obj1
에서는 obj1
을 obj2
에서는 obj2
를 가리킨다.
this
는 값을 할당
한다고 하지 않고, 바인딩
한다고 한다. 바인딩
은 식별자
와 값
을 연결해주는 것을 말하고, 할당
은 식별자
에 값을 주는 것을 말한다. 언뜻보면 비슷해 보이지만, 바인딩
이 할당
보다 더 포괄적인 개념이라고 이해하면 쉽다. 우리는 this
를 선언
한 적도 없고 값을 직접적으로 준 적도 없다. 값을 직접적으로 주려면 아래와 같은 코드가 가능해야 하지만, 문법 에러가 발생한다.
const obj = {
name: 'a',
}
const this = obj; // SyntaxError: Unexpected token 'this'
console.log(this);
this
의 필요성에 대해 이야기 하면서 this
는 객체
자기 자신을 가리킨다고 했다. 그런데 사실 this
는 상황에 따라 가리키는 값이 다르다. this
가 객체
자기 자신을 가리키는 경우는 객체 리터럴
내부에서 사용한 경우다. this
는 심지어 같은 코드의 경우에도 값이 다를수도 있다.
function Func() {
console.log(this)
}
Func(); // Window 객체 { ... }
new Func(); // Func { __proto__: { constructor: ƒ Func() } }
Func()
는 일반 함수
로 호출했고, new Func()
는 생성자 함수
로 호출해서 인스턴스
를 생성했다. 그런데 this
의 출력되는 값이 다르다. 일반 함수
로 호출한 경우에는 전역 객체인 window
를 참조하고 있고(node
환경이라면 global
객체), 생성자 함수
로 호출한 경우에는 새로운 객체가 있다. this
는 프로퍼티
와 메서드
를 사용하기 위해 객체
를 참조할 수 있는 식별자
라고 했다. 그렇기 때문에 객체를 생성하지 않는 일반 함수
에서 this
는 필요없다. 그래서 일반 함수
로 호출한 경우에 전역 객체를 참조한다. (사실 일반함수 자체도 객체이긴 한데)
그리고 생성자 함수
를 호출했을 때 this
의 값으로 나온 객체는 생성된 인스턴스
다. 생성된 인스턴스
는 기본적으로 프로토타입
을 가지고 있기 때문에, constructor
키의 값으로 Func.prototype
를 가진 객체도 출력되는 것이다.
이처럼 this
는 값이 미리 정해지는 것이 아니라, 함수가 호출
되는 방식에 따라 바인딩
되는 값이 동적으로 변경된다.
const Func = () => {
console.log(this);
}
Func(); // Window {}
new Func(); // TypeError: Func is not a constructor
Func
함수는 new
와 같이 호출해서 생성자 함수
로 동작해야 한다. 하지만 이상하게도 this
의 값이 찍히지 않고, 타입 에러가 발생했다. 그 이유는 함수의 내부 메서드인 [[Construct]]
에 있다. 자바스크립트에서는 함수도 객체이기 때문에 메서드를 가질 수 있는데 함수가 평가될 때 내부 메서드로 [[Call]]
과 [[Construct]]
가 생긴다. 그래서 함수가 호출되는 방식에 따라 일반 함수
로 호출하면 [[Call]]
이 동작하고, 생성자 함수
로 호출하면 [[Constuct]]
가 동작한다. 그래서 new
키워드와 함께 함수를 호출했을 때 생성자 함수
로 호출했는지 자바스크립트 엔진이 알 수 있다. 그런데 모든 함수가 [[Construct]]
내부 메서드를 가지는 것은 아니다. 화살표 함수
와 메서드 축약형
표현은 [[Construct]]
내부 메서드를 가지지 않고, [[Call]]
만 가지고 있다. 즉, 생성자 함수
로서 동작할 수 없다. 생성자 함수
로서 동작할 수 없기 때문에 화살표 함수
내부에서 this
를 출력하려고 했을 때 에러를 발생시킨다.
const obj = {
func() {
console.log(this)
}
}
const Func = obj.func;
new Func(); // TypeError: Func is not a constructor
메서드 축약
표현도 내부에서 생성자 함수
로 호출해서 this
를 출력하려고 하면 타입에러를 발생시킨다.