본문 바로가기

Coding/TIL (Today I Learned)

[JavaScript] This의 특징과 5가지 패턴

this의 개념을 이해했다고 생각했다가도 다른 사람이 쓴 코드를 읽을 때는 this가 무엇을 가리키고 있는지 헷갈리곤 한다. 자바스크립트에서 this 키워드는 모든 함수 scope 내에서 자동으로 설정되는 특수한 식별자로, 함수가 실행되는 동안 이용할 수 있다. 

 

this는 5가지 패턴을 가지고 있는데 엄격 모드(strict mode)와 비 엄격 모드에서도 일부 차이가 있었다. 공부를 하면서 코드의 문맥을 잘 읽기 위해서 this의 패턴을 잘 알아둘 필요를 느꼈다. 익숙해지고 있지만 글로 정리하면서 한번 더 개념을 잡고자 한다.


this의 특징

  • this의 값은 함수를 호출한 방법이 결정한다.
  • 실행하는 중 할당으로 설정할 수 없고 함수를 호출할 때 마다 다를 수 있다.
  • 함수를 어떻게 호출했는지와 무관하게 this값을 고정하기위해 bind메서드를 사용할 수 있다.
  • 화살표 함수는 this 바인딩을 제공하지 않는다.

 

this의 5가지 패턴 (Binding patterns)

01. Global : window 

전역 실행 문맥 global execution context, 즉 아무 함수에도 속하지 않은 범위에서의 this는 엄격 모드 여부에 관계없이 전역 객체를 참조한다. 브라우저 내에서는 window를 가리킴.

console.log(this);  //Window

 

02. Function 호출: window

함수 내부에서 this 의 값은 함수를 호출한 방법에 따라 값이 달라진다.

strict mode 가 아닌 아래 예제의 경우 this값이 호출에 의해 설정되지 않으므로 기본값으로 전역 객체를 참조한다. 

function f1() {
  return this;
}

// 브라우저
f1() === window;  // true 

// Node.js
f1() === global;  // true

 

strict mode인 아래 예제의 경우 this값은 실행 문맥에 진입하며 설정되는 값을 유지하기 때문에 아래의 this는 undefined가 된다. (실행 문맥이 this의 값을 설정하지 않았으므로 undefined)

function f2(){
  "use strict"; // 엄격 모드 참고
  return this;
}

f2() === undefined; // true

 

이번에는 함수 안에 내부함수가 존재하는 두 예제다. 함수의 내부에 존재하는 함수라 하더라도 규칙은 다르지 않음을 알 수 있다. 

var a = 'Global';

function outer() {
  function inner() {
    let a = 'Custom';
    console.log(this.a);
  }
  inner();
}

outer();  //Global
var a = 'Global';

function outer2() {
  var closure = function() {
    let a = 'Custom';
    console.log(this.a);
  }
  return closure;
}

outer2()();  //Global

 

03. Method 호출: 부모 object

함수를 어떤 객체의 속성에 저장하는 경우, 이 함수를 메서드라고 부른다. 함수를 어떤 객체의 메서드로 호출하면 this값은 그 객체를 사용한다. 아래 예제에서 함수 내부의 this는 객체의 val 을 가리킴을 확인할 수 있다.

let counter = {
  val: 0,
  increment: function() {
    this.val++;
  }
};

counter.increment();
console.log(counter.val);  //1
counter.increment();
console.log(counter.val);  //2

 

메서드 호출의 경우 함수가 정의된 방법이나 위치에 영향을 받지 않는 것에 주의해야 한다. 이게 무슨말인가 하면, 아래 첫 번째 예제에서는 f 함수를 o의 내부에 정의하였다.  그러나 두 번째 코드는 함수를 먼저 정의하고 나중에 o.f 를 추가했는데 아래 두 코드의 결과는 같다. 즉 함수가 o의 멤버 f로부터 호출된 것만이 중요하다는 것.

var o = {
  prop: 37,
  f: function() {
    return this.prop;
  }
};

console.log(o.f());  //37
var o = {prop: 37};

function independent() {
  return this.prop;
}

o.f = independent;

console.log(o.f());  //37

 

04. Constructor 호출: 새로 생성된 객체

함수를 new 키워드와 함께 생성자로 사용하면 this는 새로 생성된 객체에 묶인다.

function F(v) {
  this.val = v;
}

var f = new F('WooHoo!'); //F의 인스턴스를 생성

console.log(f.val);  //WooHoo!
console.log(val);  //ReferenceError

이때 만약 함수가 객체를 리턴하고 있다면 그 객체가 인스턴스의 결과값이 된다.

function C2() {
  this.a = 37;
  return {a: 38};
}

let o = new C2();

console.log(o.a);  //38

 

05. call(), apply() 호출: 첫 번째 매개변수로 명시된 객체

this의 값을 한 문맥에서 다른 문맥으로 넘기려면 call()이나 apply() 메서드를 사용해 특정한 객체에 묶을 수 있다. call()이나 apply()의 첫 번째 매개변수로 객체를 제공하면 this가 그 객체에 묶임.

const obj = {a: 'Custom'};

var a = 'Global';

function whatsThis() {
  return this.a;
}

whatsThis();           // 'Global'
whatsThis.call(obj);   // 'Custom'
whatsThis.apply(obj);  // 'Custom'

아래 코드와 같이 call()의 인자를 this로 넣어 identify.call(this)로 넣을 수 도 있다.

function identify() {
  return this.name.toUpperCase();
}

function speak() {
  var greeting = "Hello, I'm " + identify.call(this);
  console.log(greeting);
}

var me = {name: "Kim"};
var you = {name: "Lee"};

identify.call(me);  //KIM
identify.call(you);  //LEE
speak.call(me);  //Hello, I'm KIM
speak.call(you);  //Hello, I'm LEE

여기서 call()과 apply()은 같은 역할을 하지만 인자를 받는 형식에 차이가 있다. 

let add = function(x,y) {
  this.val = x + y;
}

let obj = {
  val: 0
};

add.apply(obj, [2,8]);  //인자를 배열 형식으로 받는다
console.log(obj.val);  //10

add.call(obj, 2, 8);  //인자를 각각 받는다
console.log(obj.val);  //10

 

bind()

이어서 바인드에 관하여. bind()는 호출 방법과 관계없이 특정 this값으로 호출되는 함수를 만들 수 있다. 

나도 종종 했던 실수가, 아래 예시와 같이 객체로부터 메소드를 추출한 뒤 그 함수를 호출할 때 원본 객체가 그 함수의 this로 사용될 것이라 생각했던 점 이다. bind()로 원본 객체가 바인딩되는 함수를 생성하면 이러한 혼동을 줄일 수 있다.

var x = 9;
var module = {
  x: 81,
  getX: function() { return this.x; }
};

module.getX();  //81

var retrieveX = module.getX;  //객체로부터 추출된 메서드의 this는 어디를 가리킬까?
var boundGetX = retrieveX.bind(module);

retrieveX();  //9 -> 이 경우 함수가 전역 스코프에서 호출됨
boundGetX();  //81

 

화살표 함수에서의 this

마지막으로 화살표 함수에서 this는 자신을 감싼 정적 범위 Lexical context 를 가리킨다. 전역 코드에서는 전역 객체를 가리킨다. 화살표 함수를 call(), bind(), apply()를 사용해 호출할 때, this의 값을 정해주더라도 무시됨을 기억하자. 사용할 매개변수를 정해주는 건 문제없지만, 첫 번째 매개변수(thisArg)는 null을 지정해 주어야 한다.

var globalObject = this;
var foo = (() => this);

console.log(foo() === globalObject);  // true

// 객체로서 메서드 호출
var obj = {func: foo};
console.log(obj.func() === globalObject); // true

// call()로 this 설정 시도
console.log(foo.call(obj) === globalObject); // true

// bind()로 this 설정 시도
foo = foo.bind(obj);
console.log(foo() === globalObject); // true