Please enable JavaScript to view the comments powered by Disqus.

자바스크립트의 this가 가리키는 것

자바스크립트에서 컨텍스트(context)는 곧 객체 인스턴스다. 그리고 컨텍스트는 this라는 키워드를 통해 참조할 수 있다. 자바스크립트에서 this는 처음 접했을 때 많은 혼란을 주는 존재다, 이 키워드는 사용되는 위치에 따라 서로 다른 객체를 가리키기 때문이다. this 키워드를 파악하기 위해서는 this를 사용하고 있는 함수의 객체 생성 여부와 함수와 객체 사이의 관계를 잘 살펴봐야 한다.

이 글에서 언급하는 ‘객체’란 인스턴스를 의미한다. 즉 new 키워드를 통해 실체화 된 객체를 말한다.

함수

먼저 함수 내부에서의 this를 살펴보자. 함수는 선언한 후, 호출해서 사용한다.

function foo() {
  console.log('this is', this)
}

foo() // this is undefined

위의 코드를 실행하면 콘솔 창에는 undefined가 출력된다. 단순히 선언만 한 함수는 컨텍스트가 없기 때문이다. 호출을 통해 내부 프로세스를 사용할 수는 있지만 함수라는 틀만 있고 객체 인스턴스로는 만들어지지 않은 상태다. 실체가 없으므로 함수 내부에서 thisundefined가 된다.

이 글의 모든 예제는 전역 공간(브라우저의 콘솔 커맨드라인에서 실행하는 것과 같다.)에서 Strict 모드로 실행했다. Strict 모드를 사용하면 this 객체가 null 또는 undefined일 때 전역 객체로 자동 변환되지 않는다. 만약 Strict 모드를 사용하지 않았다면 위의 예제에서는 undefined 대신 Window가 출력되었을 것이다.

함수로 객체를 생성한다는 개념이 약간 어색하게 느껴질 수 있지만, 자바스크립트에는 가능한 일이다. 자바스크립트에서는 객체 리터럴 표기법(ex. var a = { prop: 'value' })뿐만 아니라 new 키워드와 함수를 사용해서 객체를 만들 수 있다.

앞서 선언한 foo 함수로 객체를 만들어 보자. 그러면 콘솔 로그에는 함수를 직접 호출했을 때와 다른 결과가 나타난다.

var f = new foo() // this is foo {}

앞에서는 thisundefined라고 했지만 여기서는 foo라고 한다. this가 가리키는 대상이 바뀐 이유는 함수 foo를 바탕으로 한 객체가 만들어졌기 때문이다. 따라서 console.log 함수에서 참조한 this가 가리키는 대상은 이제 foo가 된다.

메소드(method)

객체의 속성에 할당된 함수를 메소드라고 한다. 메소드 내부에서 this가 무엇을 가리키는지 확인해보자.

function foo() {
  this.check = function() {
    console.log('this is', this)
  }
}

var f = new foo()
f.check() // this is foo {check: ƒ}

check 메소드 내부의 thisfoo로 나온다. 메소드도 앞서 언급한 것처럼 함수 선언만 하고 객체가 생성되지는 않은 상태다. 하지만 함수 선언과 달리 메소드는 foo 함수의 컨텍스트(this)에 직접 연결되어 있다. foo 함수로 객체가 만들어지면 메소드도 그 객체에 포함된다. 서로 동일한 컨텍스트를 공유하는 개념이라고 볼 수 있다.

클래스 문법으로 프로토타입 메소드를 선언하거나 리터럴 문법으로 선언한 객체에도 같은 법칙이 적용된다.

class Bar {
  check() { // === Bar.prototype.check
    console.log('this is', this)
  }
}

var b = new Bar()
b.check() // this is Bar {}
var obj = {
  checkAtObj: function() {
    console.log('this is', this)
  }
}
obj.checkAtObj() // this is {checkAtObj: ƒ}

컨텍스트를 가지지 않는 화살표 함수

ES6에는 화살표 함수 문법이 추가되었다. 화살표 함수로 생성된 함수는 컨텍스트를 가지지 않으며 객체 생성을 위한 constructor도 없어서 new 키워드로 객체를 생성할 수도 없다.

화살표 함수는 일반적인 함수 선언과 다르게 작동한다. 화살표 함수 내부에서 this는 그 함수를 포함하고 있는 객체 인스턴스를 가리킨다. 함수처럼 사용할 수 있지만 화살표 함수 내부의 코드는 중괄호로 둘러싸이지 않은 영역에서 실행된다고 생각하면 이해하기 쉽다.

function test() {
  function normalFunc() {
    console.log('this in normalFunc is', this) // this in normalFunc is undefined
  }
  normalFunc()

  var arrowFunc = () => {
    console.log('this in arrowFunc is', this) // this in arrowFunc is test {}
  }
  arrowFunc()

  console.log('this', this) // this test {}
}

var testInstance = new test()

컨텍스트를 직접 지정하기

지금까지는 살펴본 것들은 자연스러운 상황이었다. 하지만 자바스크립트는 컨텍스트, 즉 this가 가리키는 객체를 조작할 수 있는 방법을 제공한다. Function 함수의 프로토타입에 선언되어 있는 call, apply, bind가 그것이다. 이 3가지 메소드는 사용 방식이 조금씩 다를 뿐 목적은 모두 동일하다.

call은 컨텍스트를 지정하는 동시에 호출한다.

function test() {
  this.check = function () {
    console.log(this)
  }
}
var testInstance = new test()

testInstance.check() // test {check: ƒ}
testInstance.check.call(window) // Window {}

자바스크립트의 모든 함수는 기본 객체인 Function으로 만들어진 객체이기에 사용자가 만들지 않은 속성을 프로토타입으로 가지고 있다. call 메소드에 객체를 파라미터로 전달하면 함수가 즉시 실행되면서 this만 바뀐다. apply는 call과 같지만, 원본 함수에 전달할 파라미터를 배열에 담아서 전달한다는 것만 다르다.

testInstance.check.call(window, 'arg1', 'args2')
testInstance.check.apply(window, ['arg1', 'args2'])

앞의 두 메소드와 달리 bind는 즉시 실행되지 않는다. 대신 고차 함수가 하는 것처럼 새로운 함수를 만든다.

const calc = {
  base: 10,
  sum: function (num) {
    return this.base + num
  }
}
const hundred = {
  base: 100
}

console.log(calc.sum(1)) // 11

// sum 메소드의 컨텍스트를 교체해서 this.base가 100이 되도록 한다.
const sumBindToHundred = calc.sum.bind(hundred)

console.log(sumBindToHundred(1))  // 101

컨텍스트? this? 결국 모두 객체다.

자바스크립트는 모든 것이 객체로 구성되어 있으며 this도 결국 객체를 가리키는 키워드다. 하지만 몇 가지 변형이 있어서 모든 사례에 적용되는 규칙을 정의하기는 어렵다. 그래도 this를 관통하는 추상적인 원칙을 하나 들자면 this는 실체가 있는 대상을 가리켜야 한다가 아닐까 한다. 단순 함수 선언은 컨텍스트가 없고, 객체가 만들어진 함수만 컨텍스트가 있기 때문이다. 그리고 메소드로 지정된 함수는 객체로 만들어지진 않았지만 다른 객체에 포함되어 있으니 실체가 있다고 간주할 수도 있지 않을까? 개인적으로는 this를 단순 암기식으로 외우는 것보다 이런 식으로 원칙을 세우는 편이 이해하기에 좋았다.