[JavaScript] this 이해하기

목적

대표적으로 JAVA 같은 객체지향 언어에서의 this는 클래스 인스턴스의 레퍼런스 변수입니다. 하지만 javacript에서 this는 전혀 다른 의미를 가집니다. this가 어려운 이유는 자바스크립트에서 this는 전혀 다른 의미를 가지기 때문입니다. 이 글은 공부를 목적으로 정리한 글입니다.

this란?

this는 현재 실행 문맥이다.

“실행 문맥”이란 말은 호출자가 누구냐는 것과 같습니다.

alert(this === window) //true, 호출자는 window

const caller = {
  f: function() {
    alert(this === window)
  },
}
caller.f() // false, 호출자는 caller 객체

평상시의 this는 window를 가리킵니다. 첫 번째 alert는 window.alert() 즉 window 객체의 메소드 호출이기 때문에 호출자는 window이고 두 번째는 caller 객체의 호출입니다. 다만 strict-mode에서는 전역 객체냐 일반 객체냐에 따라 함수 내부의 this의 결과가 차이가 있습니다. 이 문제는 window를 함수 호출 앞에 붙여주면 해결됩니다.

function nonStrictMode() {
  return this
}

function strictMode() {
  'use strict'
  return this
}

console.log(nonStrictMode()) // window
console.log(strictMode()) // undefined
console.log(window.stricMode()) //window

생성자 함수 / 객체에서는 어떻게 쓰이나?

생성자에는 우리가 제일 싫어하는 this가 많이 나옵니다. 여기서 생성자 함수도 함수입니다. 생성자는 new로 객체를 만들어 사용합니다. 만약에 new로 호출하지 않고 그냥 호출을 하게 되면 어떻게 될까요?

function NewObject(name, color) {
  this.name = name
  this.color = color
  this.isWindow = function() {
    return this === window
  }
}

const newObj = NewObject('nana', 'yellow')
console.log(newObj.name) // error
console.log(newObj.color) // error
console.log(newObj.isWindow()) // error

const newObj2 = new NewObject('didi', 'red')
console.log(newObj2.name) // didi
console.log(newObj2.color) // red
console.log(newObj2.isWindow()) // false

new를 붙이지 않고 그냥 호출하면 어떻게 될까요? 위에서 그냥 함수에서 this가 window를 가리킨다고 말했습니다. 이렇게 되면 각 property를 가져올 수 없게 됩니다. 생성자 함수가 아닌 일반 객체에서는 어떨까요?

const person = {
  name: 'john',
  age: 15000,
  nickname: 'man from earth',
  getName: function() {
    return this.name
  },
}
console.log(person.getName()) //john

const otherPerson = person
ortherPerson.name = 'chris'
console.log(person.getName()) // chris
console.log(otherPerson.getName()) //chris

생성자 함수와 크게 다르지 않습니다. 한 가지 볼 점은 otherPerson.name을 chris로 설정한 뒤 person.getName()을 호출하면 출력 결과는 chris로 변경됩니다. 이유는 otherPerson은 person의 레퍼런스 변수이므로 하나를 변경하면 다른 하나도 변경됩니다. 이를 피하기 위해 Object.assign() 메서드를 이용하여 완전히 별도의 객체로 만들어야 합니다.

const person = {
  name: 'john',
  age: 15000,
  nickname: 'man from earth',
  getName: function() {
    return this.name
  },
}
const newPerson = Object.assign({}, person)
newPerson.name = 'chris'
console.log(person.getName()) // john
console.log(newPerson.getName()) // chris

bind, arrow function

이번에는 생성자 함수 안에서 또 다른 함수가 있는 경우를 살펴보겠습니다.

function Family(firstName) {
  this.firstName = firstName
  const names = ['bill', 'mark', 'steve']
  names.forEach(function(lastName, index) {
    console.log(lastName + ' ' + this.firstName)
    console.log(this)
  })
}
const kims = new Family('kim')
// bill undefined
// window
// mark undefined
// window
// steve undefined
// window

Family 생성자 함수 안에서 forEach 메서드를 호출합니다. undefined가 출력되는 이유는 forEach 메서드의 서브루틴은 호출될 때 forEach의 context(this)로 바인드 되지 않습니다. 바인드가 안되었다는 것은 전역이라는 것이고 실행 문맥이 전역이라는 말은 this가 window라는 것입니다.

이 문제를 해결하기 위해서는 this를 that이라는 변수에 저장하면 됩니다.

function Family(firstName) {
  this.firstName = firstName
  const names = ['bill', 'mark', 'steve']
  const that = this
  names.forEach(function(value, index) {
    console.log(value + ' ' + that.firstName)
  })
}
const kims = new Family('kim')
// bill kim
// mark kim
// steve kim

또 다른 방법은 bind라는 메서드를 사용하면 됩니다.

function Family(firstName) {
  this.firstName = firstName
  const names = ['bill', 'mark', 'steve']
  names.forEach(
    function(value, index) {
      console.log(value + ' ' + this.firstName)
    }.bind(this)
  )
}
const kims = new Family('kim')

또는 ES6 화살표 함수(arrow function)를 사용할 수 있습니다.

function Family(firstName) {
  this.firstName = firstName
  const names = ['bill', 'mark', 'steve']

  name.forEach((value, index) => {
    console.log(value + ' ' + this.firstName)
  })
}
const kims = new Family('kim')

정리

  • this는 다른 언어와 다른 방식으로 사용된다.
  • this는 누가 호출했느냐에 따라 결정된다.
  • 화살표 함수를 쓰자.

참고