자바스크립트에서 this란

Jin·2022년 3월 1일
0

Javascript

목록 보기
10/22

this를 둘러싼 오해

this는 모든 함수 스코프 내에 자동으로 설정되는 특수한 식별자입니다.

this는 어떤 식으로도 함수의 렉시컬 스코프를 참조하지 않습니다.

function foo() {
  var a = 2;
  this.bar();
}

function bar() {
  console.log(this.a);
}

foo(); // error

foo()와 bar()의 렉시컬 스코프 사이에 어떤 연결 통로를 만들어서 bar가 foo의 내부 스코프에 있는 변수 a에 접근하게 하고 싶지만 그건 불가능합니다.

this는 작성 시점이 아닌 런타임 시점에 바인딩되며 함수 호출 호출 당시 상황에 따라 콘텍스트가 결정됩니다.

렉시컬 스코프가 함수 선언문 위치가 중요하다면 this 바인딩은 오로지 어떻게 함수를 호출했느냐에 따라 정해집니다.

this는 함수 자신이나 함수의 렉시컬 스코프를 가리키는 레퍼런스가 아니라는 점을 분명히 인지해야 합니다.


this 바인딩 방법들

1. 기본 바인딩 규칙

가장 평범한 함수 호출인 단독 함수 실행에 관한 규칙입니다.

나머지 규칙에 해당하지 않을 경우 적용되는 this의 기본 규칙입니다.

function foo() {
  console.log(this.a);
}

var a = 2;
foo(); // 2

foo 함수 호출시 this.a는 전역 변수 a입니다. 기본 바인딩이 적용되어 this는 전역 객체를 참조하기 때문입니다.

기본 바인딩 규칙이 적용됐다는 것은 함수의 호출부를 보면 알 수 있습니다.

foo()는 지극히 평범한 있는 그대로의 함수 레퍼런스를 호출하였습니다. 이것은 나머지 규칙을 논할 여지도 없이 기본 바인딩이 그대로 적용된다는 의미입니다.

엄격 모드 (strict mode)에서는 전역 객체가 기본 바인딩 대상에서 제외되므로 this가 전역 객체가 아닌 undefined가 됩니다.

function foo() {
  console.log(this.a);
}

var a = 2;
foo(); // error

this의 a 프로퍼티를 참조하려고 하지만 this가 undefined이므로 error가 발생하게 됩니다.

그래서 보통 this 바인딩 규칙은 오로지 호출부에 의해 좌우되지만 비엄격 모드에서는 전역 객체만이 기본 바인딩의 유일한 대상이 됩니다.

2. 암시적 바인딩

호출부에 컨텍스트 (context) 객체가 있는지 여부를 확인하여 있으면 그 컨텍스트 객체가 this가 되는 경우입니다.

function foo() {
  console.log(this.a);
}

var obj = {
  a: 2,
  foo: foo
}

obj.foo();

위의 코드를 보면 호출에서 obj 컨텍스트로 foo()를 참조하고 있습니다. 이러한 컨텍스트 객체가 함수 호출시 존재하면 this에 바인딩되므로 여기서 this는 obj가 됩니다.

여기서, 헷갈리는 점이 암시적으로 바인딩된 함수에서 바인딩이 소실되는 경우입니다. 그것을 '암시적 소실'이라고 합니다.

function foo() {
  console.log(this.a);
}

var obj = {
  a: 2,
  foo: foo
}

var bar = obj.foo;
var a = "nothing";

bar(); // nothing

bar는 obj의 foo를 참조하는 변수처럼 보이지만 실은 foo를 직접 가리키는 또 다른 레퍼런스입니다. 호출부에서 평범하게 bar()를 호출하므로 기본 바인딩 규칙이 적용되어 전역 객체가 this가 되므로 nothing이 출력됩니다.

3. 명시적 바인딩

어떤 객체를 this 바인딩에 이용하겠다고 코드로 명확히 밝히기 위한 유틸리티가 존재합니다.

바로 call()과 apply() 메서드입니다. 두 메서드는 this에 바인딩할 객체를 첫번째 인자로 받아 함수 호출시 이 객체를 this로 세팅합니다. this를 지정한 객체로 직접 바인딩하므로 이를 명시적 바인딩이라고 합니다.

function foo() {
  console.log(this.a);
}

var obj = {
  a: 2
}

foo.call(obj); // 2

foo.call()에서 명시적으로 바인딩하여 함수를 호출하였으므로 this는 반드시 obj가 됩니다.

bind() 유틸리티도 직접 this를 규정할 수 있습니다. 하지만 call, apply와 다른 점은 주어진 this 컨텍스트로 원본 함수를 호출하도록 하드 코딩된 새 함수를 반환한다는 것입니다.

function foo(something) {
  console.log(this.a, something);
  return this.a + something;
}

var obj = {
  a: 2
}

var bar = foo.bind(obj);
var b = bar(3); // 2 3
console.log(b); // 5

여기서 bar는 this가 obj로 고정된 foo 함수 코드 그 자체입니다. 따라서, b가 bar 함수 호출의 결과값을 담을 수 있게 되고 이 과정에서 값들이 출력되는 것입니다.

4. new 바인딩

new 바인딩에 대해 설명하기 전에 일단 자바스크립트 (이하 JS)에 관한 오해 하나를 바로잡고 가겠습니다.

전통적인 클래스 지향 언어의 생성자는 클래스에 붙은 특별한 메서드로서 클래스 인스턴스 생성시 new 연산자로 호출합니다. 하지만, JS에서 new는 의미상 클래스 지향적인 기능과 아무 관련이 없습니다.

JS에서 new를 앞에 붙여서 호출하는 것은 함수를 생성하는 호출이라고 생각하면 되겠습니다.

함수 앞에 new를 붙여 생성자 호출을 하면 다음과 같은 현상이 벌어집니다.

  • 새 객체가 만들어집니다.
  • 새로 생성된 객체의 [[Prototype]]이 연결됩니다.
  • 새로 생성된 객체는 해당 함수 호출시 this로 바인딩됩니다.
  • 이 함수가 자신의 또 다른 객체를 반환하지 않는 한 new와 함꼐 호출된 함수는 자동으로 새로 생성된 객체를 반환합니다.

따라서, new 바인딩은 함수 호출시 this를 새 객체와 바인딩하는 방법입니다.

그러면 여기서 우선순위는 어떻게 될까요?

new 바인딩 > 명시적 바인딩 > 암시적 바인딩 > 기본 바인딩 규칙 순으로 우선순위가 설정되어 있습니다.

function foo(something) {
  this.a = something;
}

var obj1 = {};

var bar = foo.bind(obj1);
bar(2);
console.log(obj1.a); // 2

var baz = new bar(3);
console.log(obj1.a); // 2
console.log(baz.a); // 3

위의 코드를 보면 알 수 있듯이 new 바인딩으로 새로운 객체에 this를 바인딩한 경우에는 기존에 this로 바인딩된 객체에 영향을 미치지 않습니다.

new 바인딩이 유용한 이유는 기본적으로 명시적인 this 바인딩을 무시하는 함수를 생성 (새로운 객체)하여 함수 인자를 전부 또는 일부만 미리 세팅할 때 있기 때문입니다.

function foo(p1, p2) {
  this.val = p1 + p2;
}

var bar = foo.bind(null, "p1");
var baz = new bar("p2");
console.log(baz.val); // p1p2

foo.bind의 첫 번째 인자인 null은 어차피 new로 호출할 때 오버라이드되므로 신경 쓰지 않겠다는 의미입니다. 바로 두 번째 인자로 "p1"를 넣어주게 되면 우리는 new bar('인자')로 호출할 때 이 '인자'가 자동으로 2번째 인자로 들어가게 되는 것입니다. (하나 이상의 인자를 넣어주고 싶으면 배열 형태로)

자, 이제 this를 확정하는 규칙을 보여드리겠습니다.

  1. new로 함수를 호출했는가? --> 맞으면 새로 생성된 객체가 this입니다.
  2. call과 apply로 함수를 호출했는가? --> 맞으면 명시적으로 지정된 객체가 this입니다.
  3. 함수를 컨텍스트 상에서 호출했는가? --> 맞으면 바로 이 콘텍스트 객체가 this입니다.
  4. 그 외의 경우는 this가 기본값으로 세팅됩니다. (undefined or 전역 객체)

바인딩 예외

this 무시

call, apply, bind 메서드에 첫 번째 인자로 null 또는 undefined를 넘기면 this 바인딩이 무시되고 기본 바인딩 규칙이 적용됩니다.

null나 undefined보다 더 안전하게 빈 객체를 만들고 싶다면 Object.create(null)를 변수에 담아서 표현하면 됩니다.

Object.create(null)은 {}보다 더 텅 빈 객체라고 볼 수 있기 때문입니다. ('자바스크립트에서 프로토타입이란' 참고)

간접 레퍼런스

function foo() {
   console.log(this.a);
}

var a = 2;
var o = {a: 3, foo: foo};
var p = {a:4};
o.foo(); // 3
(function() {
  p.foo = o.foo;
  p.foo(); // 4
  })();

위의 코드에서 p.foo = o.foo는 앞에서도 말했다시피 o.foo가 바로 직접적으로 foo 함수를 참조하게 되므로 기본 바인딩 규칙이 적용됩니다.

화살표 함수

ES6부터는 위의 this 바인딩 규칙들을 따르지 않는 특별한 함수가 있는데 그게 바로 화살표 함수입니다.

화살표 함수는 4가지 표준 규칙 대신 스코프를 보고 this를 알아서 바인딩합니다.

function foo() {
  setTimeout(() => {
    console.log(this.a);
  }, 100);
}

var obj = {
  a: 2
};

foo.call(obj); // 2

화살표 함수가 아니었다면 2가 아닌 undefined가 출력값이 되었을 것입니다.
하지만, 화살표 함수가 setTimeout 함수 내부에서도 foo 함수 내부의 this인 obj를 this로 계속 참조하고 있기 때문에 2가 출력됩니다.

여기서 많은 개발자들은 setTimeout 함수를 사용하기 이전에 var self = this; 를 선언하여 self를 활용합니다.
하지만 이것은 this에서 도망치려는 꼼수입니다.

this와 관련하여 확실히 하여야하는 코드 스타일은 다음과 같습니다.

  • 오직 렉시컬 스코프만 사용하고 this 스타일의 코드는 사용하지 않는다.
  • 필요하면 bind()까지 포함하여 완전한 this 스타일의 코드를 구사하되 self = this나 화살표 함수는 삼가야 한다.
  • 서로 다른 스타일이 하나의 함수에 혼용되면 관리도 힘들고 이해하기 곤란하므로 혼용하지 않는다.
profile
배워서 공유하기

0개의 댓글