[js] this

김효식 (HS KIM)·2021년 12월 14일
0

자바스크립트의 this는 상황에 따라 값이 다르기 때문에, 현재 상황에서 어떤 값을 가지고 있는지 매우 헷갈린다. 오늘도 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

그러나 메서드를 호출한 객체메서드를 정의한 객체라는 보장은 없다. obj2getIdobj1getId를 참조하고 있다. 바람직한 코드는 아니지만, 메서드함수이기 때문에 다른 메서드변수으로 할당할 수 있다. obj2.getId()obj2id값을 출력할거라고 기대하고 사용했지만 getIdobjid를 참조하고 있기 때문에 똑같이 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에서는 obj1obj2에서는 obj2를 가리킨다.

this 바인딩

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는 값이 미리 정해지는 것이 아니라, 함수가 호출되는 방식에 따라 바인딩되는 값이 동적으로 변경된다.

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를 출력하려고 하면 타입에러를 발생시킨다.

profile
자기개발 :)

0개의 댓글