자바스크립트의 this가 가리키는 것
자바스크립트에서 컨텍스트(context)는 곧 객체 인스턴스다. 그리고 컨텍스트는 this
라는 키워드를 통해 참조할 수 있다. 자바스크립트에서 this
는 처음 접했을 때 많은 혼란을 주는 존재다, 이 키워드는 사용되는 위치에 따라 서로 다른 객체를 가리키기 때문이다. this
키워드를 파악하기 위해서는 this
를 사용하고 있는 함수의 객체 생성 여부와 함수와 객체 사이의 관계를 잘 살펴봐야 한다.
이 글에서 언급하는 ‘객체’란 인스턴스를 의미한다. 즉
new
키워드를 통해 실체화 된 객체를 말한다.
함수
먼저 함수 내부에서의 this
를 살펴보자. 함수는 선언한 후, 호출해서 사용한다.
function foo() {
console.log('this is', this)
}
foo() // this is undefined
위의 코드를 실행하면 콘솔 창에는 undefined
가 출력된다. 단순히 선언만 한 함수는 컨텍스트가 없기 때문이다. 호출을 통해 내부 프로세스를 사용할 수는 있지만 함수라는 틀만 있고 객체 인스턴스로는 만들어지지 않은 상태다. 실체가 없으므로 함수 내부에서 this
는 undefined
가 된다.
이 글의 모든 예제는 전역 공간(브라우저의 콘솔 커맨드라인에서 실행하는 것과 같다.)에서 Strict 모드로 실행했다. Strict 모드를 사용하면
this
객체가null
또는undefined
일 때 전역 객체로 자동 변환되지 않는다. 만약 Strict 모드를 사용하지 않았다면 위의 예제에서는undefined
대신Window
가 출력되었을 것이다.
함수로 객체를 생성한다는 개념이 약간 어색하게 느껴질 수 있지만, 자바스크립트에는 가능한 일이다. 자바스크립트에서는 객체 리터럴 표기법(ex. var a = { prop: 'value' }
)뿐만 아니라 new
키워드와 함수를 사용해서 객체를 만들 수 있다.
앞서 선언한 foo
함수로 객체를 만들어 보자. 그러면 콘솔 로그에는 함수를 직접 호출했을 때와 다른 결과가 나타난다.
var f = new foo() // this is foo {}
앞에서는 this
가 undefined
라고 했지만 여기서는 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
메소드 내부의 this
는 foo
로 나온다. 메소드도 앞서 언급한 것처럼 함수 선언만 하고 객체가 생성되지는 않은 상태다. 하지만 함수 선언과 달리 메소드는 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
를 단순 암기식으로 외우는 것보다 이런 식으로 원칙을 세우는 편이 이해하기에 좋았다.