혹시 JavaScript의 this
에 대해 알고 계시나요? 흔히 우리가 알고 있는 this
는 '자기 참조 변수(self-refenrence variable)'. 즉 자신 자신을 가리키는 변수입니다. 만약 전역 컨텍스트에서 this
를 호출하게 된다면 window
객체가 호출되게 됩니다. 최상위 객체이기 때문입니다.
그렇다면 다음 문제를 통해 참조되는 this
값을 예상해 봅시다.
const o = {
name: "Kim",
changeMyName: function (name) {
this.name = name
},
};
const o2 = {
name: "Song",
};
function callFuncWithArg(f, arg) {
f(arg);
}
function Person(name){
this.name = name
}
const p = new Person('Jeong')
o.changeMyName.bind(o2)("Sam");
console.log("1번 - ", o2.name); // Sam
callFuncWithArg(o.changeMyName, "Daniel");
console.log("2번 - ", o.name); // Kim
o.changeMyName("Sam");
console.log("3번 - ", o.name); // Sam
console.log('4번 - ', p.name) // Jeong
답은 위에 나와있는 코드와 같이 나타나게 됩니다. 그렇다면 왜 다음과 같은 경우가 나타나게 되는 것일까요? 우리가 this에 대해 알아야 할 중요한 특징 중 하나는 "this
가 가리키는 값은 함수 호출 방식에 따라 결정된다는 것" 입니다.
this
가 결정되는 방식에는 크게 4가지가 있습니다.
기본적으로 전역 객체 window
가 바인딩 됩니다. 하지만 평소 this
의 용도를 생각하면 일반 함수에서 this
는 의미가 없습니다. strict mode에서는 undefined
가 바인딩 됩니다.
콜백 함수가 일반 함수로 호출된다면 콜백 함수 내부의 this
는 window
가 바인딩 됩니다. 어떤 함수라도 일반 함수로 호출한다면 this
에 window
가 바인딩 됩니다.
위와 같은 예시는 위 코드에서 2번 호출을 통해 알 수 있습니다. 2번을 보면 함수 실행할 콜백 함수와 그 인자를 매개변수로 전달합니다. 하지만, callFuncWithArg
라는 일반 함수 호출이 되기 때문에 전달받은 o.changeMyName
에서 가리키는 this
는 객체 o
가 아닌 window
가 됩니다. (실제로 window.name에 Daniels라는 변수가 저장되어 있습니다.)
이처럼 보조 함수의 this
와 메서드 내의 this
가 다른 것을 가리키는 것은 메서드 동작에 있어서 잘못된 오류를 발생시키기 충분합니다. 이를 해결하기 위해서 화살표 함수를 사용하거나 call/bind/apply
메서드를 통해 this
를 바인딩 하여 사용합니다.(1번 방식 참조)
메서드는 객체_이름. 메서드_이름
의 형식으로 호출됩니다. 여기서 주의해야 할 점은 메서드 내부this
는 메서드를 소유한 객체가 아닌
메서드를 호출한 객체에 바인딩
된다는 것입니다.
const person = {
name: 'Lee',
getName(){
return this.name
}
}
const anotherPerson = {
name:'Jeong'
}
anotherPerson.getName = person.getName
const getName = anotherPerson.getName
//앞에서도 언급했듯이 일반함수 호출은 this에 window를 바인딩 한다. 즉 window.name을 호출하는 것이다.
console.log(anotherPerson.getName(),person.getName(),getName()) // Jeong Lee ''
위 코드를 보면 메서드 내부의 this
는 프로퍼티 메서드를 가리키고 있는 객체와 관계없이 메서드를 호출한 객체에 바인딩 됩니다. person
객체의 프로퍼티가 가리키는 함수 각체는 person
객체에 포함된 것이 아니라 독립적으로 존재하는 별도의 객체입니다.
이는 prototype
메서드 내부에서 선언한 this
에도 똑같이 적용이 됩니다.
const person = {
name: 'Lee',
getName(){
return this.name
}
}
const anotherPerson = {
name:'Jeong'
}
anotherPerson.getName = person.getName
const getName = anotherPerson.getName
//앞에서도 언급했듯이 일반함수 호출은 this에 window를 바인딩 한다. 즉 window.name을 호출하는 것이다.
console.log(anotherPerson.getName(),person.getName(),getName()) // Jeong Lee ''
생성자 함수를 통해 생성된 인스턴스 me
가 가리키는 name
은 생성자에서 선언한 Jeong
이며, Person.prototype
객체가 가지고 있는 name
은 Kim
입니다.
생성자 함수 내부의 this
는 생성자 함수가 생성한 인스턴스가 바인딩 됩니다. (3번 예시)
p
라는 인스턴스에 new
키워드를 통해 생성자 함수를 호출하게 된다면 this
가 가리키는 것은 p
가 됩니다. 하지만, new
키워드를 사용하지 않고 호출한다면, 일반 함수와 동일하게 동작합니다.
앞서 언급했듯이, 콜백 함수의 this
와 생성자의 this
가 다른 것은 큰 혼란을 야기하게 됩니다. 이를 해결하기 위한 방법이 apply/call/bind
입니다. 해당 메서드를 통해 호출하는 함수의 this
를 원하는 객체로 지정할 수 있습니다.
/*
@param thisArg - this로 사용할 객체
@param argsArray - 함수에게 전달할 인수 리스트의 배열 또는 유사 배열 객체
@returns 호출된 함수의 반환값
*/
Function.prototype.apply(thisArg[,argsArray])
/*
주어진 this 바인딩과 ,로 구분된 인수 리스트를 사용하여 함수를 호출한다.
@param thisArg - this로 사용할 객체
@param arg1,arg2, ... - 함수에게 전달할 인수 리스트
@returns 호출된 함수의 반환값
*/
Function.prototype.call (thisArg[,arg1[,arg2[,...]]])
apply
와 call
메서드는 함수를 호출하는 것입니다. 두 메서드 모두 함수를 호출하면서 첫 번째 인수로 전달한 객체를 호출한 함수의 this
에 바인딩 합니다. 두 메서드는 인수 전달 방식만 다를 뿐이지 같은 기능을 동작합니다.
반면에 bind
메서드는 함수를 호출하지 않고 this
로 사용할 객체만 전달합니다.
function getThisBinding(){
return this
}
const thisArg = {a:1}
console.log(getThisBinding.bind(thisArg)) // getThisBinding
console.log(getThisBinding.bind(thisArg)()) //{a:1}
이처럼 bind
메서드는 메서드의 this
와 메서드 내부의 중첩 함수 또는 콜백 함수의 this
가 불일치하는 문제 해결을 위해 사용됩니다. (1번 참조)
const person = {
name:'Jeong',
foo(callback){
setTimeout(callback,100)
}
}
person.foo(function(){
console.log(this.name) // ' '
})
// 일반함수로 호출된 콜백함수의 this는 전역객체를 바인딩하기 때문에 아무것도 의도한 대로 this.name이 반환되지 않는다.
const person = {
name:'Jeong',
foo(callback){
setTimeout(callback.bind(this),100)
}
}
person.foo(function(){
console.log(this.name) // Jeong
})
오랜만에 this
에 대해 공부하다가 제일 위에 있는 문제의 2번 답을 보고 "아니 왜 Daniels가 아니지?"라는 의문에 다시 한번 공부하게 되었습니다. 예전에 책을 보면서 정리를 했다고 생각했던 개념이었지만, 사용하지 않고 얕게 훑기만 해서 그런지 처음에는 정말로 왜 그런지 몰랐습니다. 그리고 다시 한번 공부를 하니 훨씬 이해가 잘 된 개념이었습니다.
혹시 잘못된 부분이 있다면 말씀 부탁드립니다!
JavaScript Deep Dive