프로그래밍/JavaScript

[JS] this 키워드는 어떻게 동작할까?

mhko411 2021. 8. 30. 23:39
728x90

오픈 소스 프로젝트에 참여하고자 특정 프로젝트의 코드를 분석해보면서 this가 많이 쓰여져있는 것을 확인했다. 하지만 this의 정확한 동작 방법과 의미를 이해하지 못하고 있다는 것을 깨달았다. 따라서 this가 무엇이고 특정 상황에 따라 어떻게 동작하는지 공부해보려고 한다.


this의 필요성

먼저 this가 왜 필요한지 알 필요가 있다. 다음의 코드는 "모던 자바스크립트 Deep Dive"를 참고하였다. 원의 반지름을 통해 지름을 구하는 getDiameter라는 메서드가 circle 객체에 포함되어있다. 아래의 코드에서 객체 리터럴은 circle 변수에 할당되기 직전에 평가된다. 따라서 getDiameter가 호출되는 시점에는 객체 리터럴의 평가가 완료되어 circle에 생성된 객체가 할당된 상태일 것이다.

const circle = {
  radius: 3,
  getDiameter() {
    return 2 * circle.radius;
  },
};

console.log(circle.getDiameter());

하지만 위와 같이 자신이 속한 객체의 식별자를 재귀적으로 참조하는 것은 바람직하지 않다.

 

또한, 만약에 생성자 함수를 생성했을 때 인스턴스 생성 전에 자신이 속한 객체의 식별자를 재귀적으로 참조하려면 어떻게할까? 아직 인스턴스가 생성되지 않았기 때문에 참조할 대상이 존재하지 않는다. 하지만 위의 코드처럼 재귀적으로 참조하여 로직을 구성해야하는 상황이 발생한다.

이러한 상황에서 재귀적으로 참조하는 것이 아닌 this 키워드를 사용하는 것이다.


일반적으로 클래스 기반의 객체지향 언어(C++, JAVA)에서의 this는 무조건 생성된 인스턴스를 가리킨다. 하지만 자바스크립트의 경우에는 this 바인딩이 동적으로 결정되기 때문에 어떻게 호출하느냐에 따라 this가 가리키는 것이 다르다.

 

함수 호출 방식에 따른 this의 값

자바스크립트에서 함수는 일반 함수, 메서드, 생성자 함수로서 호출을 할 수 있다. 또한 apply, call, bind를 통해 간접 호출을 할 수도 있다. 이처럼 함수 호출에 따라서 this가 가리키는 값이 어떻게 변하는지 알아보자.

 

1. 일반 함수 호출

기본적으로 this는 전역 객체를 가리킨다. 브라우저에서는 window, node에서는 global을 가리킨다. 아래의 코드는 일반 함수로 호출 했을 때 this가 무엇을 가리키는지 나타내는 코드이다. 

Node.js에서 실행을 하였으며 this는 global을 가리키게된다. 뿐만 아니라 bar()의 경우에도 일반 함수로 호출하였기 때문에 this에는 global이 바인딩되며 일반 함수로 호출된 콜백 함수 내부에서도 this는 전역 객체가 바인딩된다.

function foo() {
  console.log(this); // global
  
  function bar() {
    console.log(this); // global
  }
  
  bar();
}

foo();

하지만 아래의 코드처럼 "strict mode"가 적용된다면 this는 undefined를 가리키게된다.

function foo() {
  "use strict";
  console.log(this); // undefined
  
  function bar() {
    console.log(this); // undefined
  }
  
  bar();
}

foo();

 

2. 메서드 호출

객체의 메서드에서 호출할 때 this는 메서드를 호출한 객체에 바인딩된다. 아래의 코드에서 객체 obj는 name이라는 속성과 getObj()라는 메서드를 갖고있다. getObj()를 메서드로 호출하면 this가 출력되는데 결과는 아래와 같다.

const obj = {
  name: "kim",
  getObj() {
    return this;
  },
};

console.log(obj.getObj()); // { name: 'kim', getName: [Function: getName] }

여기서 헷갈릴 수도 있는 개념이 존재한다. 객체 내에서 메서드가 가리키는 함수 객체는 객체에 포함된 것이 아니라 독립적으로 존재하는 별도의 객체이다. 그렇기 때문에 메서드 호출을 했을 때는 위와 같이 this의 결과가 나타나지만 일반 변수에 할당하여 일반 함수로 호출하면 전역 객체를 가리키게 된다.

아래의 코드를 보면 알 수 있다. obj.getThis()처럼 메서드 호출을 하면 위의 코드와 같은 결과가 나타나지만 obj의 getThis를 일반 변수에 바인딩하여 일반 함수 호출을 하면 node의 전역 객체인 global을 가리키게 된다. 이것은 위에서 말한 것처럼 객체 내의 메서드는 함수 객체에 포함된 것이 아니라 독립적으로 존재하는 별도의 객체이기 때문이다.

const obj = {
  name: "kim",
  getThis() {
    return this;
  },
};

console.log(obj.getThis());

const getThis = obj.getThis;
console.log(getThis()); // global

 

3. 생성자 함수 호출

샘성자 함수 호출을 하면 this는 이후에 생성될 인스턴스가 바인딩된다. 이때 인스턴스 생성 과정에서 new를 사용하지 않으면 일반 함수처럼 동작하기 때문에 전역 객체가 저장될 것이다.

function Person(name) {
  this.name = name;
  this.getName = () => {
    return this.name;
  };
}

const kim = new Person("kim");
console.log(kim.getName()); // kim

const lee = new Person("lee");
console.log(lee.getName()); // lee

 

4. apply / call / bind에 의한 간접 호출

아직 apply / call / bind를 사용해보지 못했고 개념을 확실히 정리한 것은 아니다. apply와 call은 모두 함수를 호출하는 역할을 하지만 약간의 차이가 존재한다. 여러 개의 인자를 전달할 때 apply는 인자를 배열로 묶어서 전달하고, call은 쉼표로 구분하여 전달한다. 아래의 코드를 보면 apply와 call의 차이를 알 수 있을 것이다.

function foo(numbers, x, y) {
  console.log(arguments);
  return this;
}

let numbers = [1, 2, 3, 4, 5];
let x = "x";
let y = "y";
console.log(foo.apply(numbers, [x, y]));
console.log(foo.call(numbers, x, y));

위의 코드를 보면 apply와 call로 함수를 호출했을 때 this에는 전달된 객체중 첫 번째 객체가 바인딩되는 것을 확인할 수 있다.


이번에 this의 필요성과 함수 호출에 따른 this의 값을 비교해보았다. 정리를 해보면 다음과 같다.

- 일반 함수 호출 : this=전역객체

- 메서드 호출 : this=메서드가 포함된 객체

- 생성자 함수 호출 : this=인스턴스

- apply/call/bind 호출 : this=인자로 전달받은 객체 중 첫 번째 객체

'프로그래밍 > JavaScript' 카테고리의 다른 글

[ES6] forEach와 map의 차이점  (0) 2021.09.06
[JS] Javascript 모듈 시스템  (0) 2021.09.01
[JS] null과 undefined의 차이  (0) 2021.08.23
[ES6] Promise  (0) 2021.08.12
[ES6] 제너레이터  (0) 2021.08.04