이번 포스트에서는 알쏭 달쏭 헤깔리는 javascript에서의 this에 대해서 살펴보자.
javascript에서의 this
javascript에서의 this가 어려운 이유는 그때 그때 달라지기 때문이다.
다른 언어에서의 this와 그 정의 부터 남다른데 java에서의 this가 현재 실행되고 있는 객체로 고정된 반면 javascript에서의 this는 함수를 호출하는 녀석에 따라 달라진다. 즉 실행 문맥(execution context)에 따라 달라진다. 여기서 실행 문맥이란 JavaScript 코드가 실행되는 환경을 의미하며 전역 실행 문맥, 일반 함수 호출, 생성자 함수 호출, try ~ catch 블록, 모듈등에서 새로운 실행 환경을 구성한다. 객체의 리터럴을 별도의 실행환경을 구성하지 않는다.
일반적인 this와 구분해 놔야 할 this
this를 사용할 때는 대부분 아래의 4가지 상황에 대해 정확히 이해하고 사용해야 한다.
전역 객체에서의 this:
객체의 메서드에서 this
apply/bind/call 함수에서의 this
생성자 함수에서 this
특히 1, 2, 3 상황에서 this 동작을 주의해서 살펴보자.
호출 방식에 따른 this의 변화
간단한 javascript 코드를 만들고 this를 살펴보자.
let dog = {
species: "보더콜리",
eat() {
console.log(`${this.species}는 잘 먹는다`, this); // 이 this의 정체를 알 수 있는 사람은 없다.
},
};
dog.eat();
dog 안에는 specises와 eat가 선언되어있고 eat에서는 this를 활용하고 있는 너무 간단한 코드이다. java 코드에 익숙한 친구들은 이 시점에서 this를 추정하려고 한다. 당연히 dog라는 객체일 것이다. 하지만 여기서 주의할 점은 javascript에서의 this는 선언 시점에 결정되지 않는다는 점이다.
일단 dog.eat()의 결과는 아마도 여러분의 예상대로 잘 출력될 것이다. 처음 생각대로 this는 dog라는 착각에 빠지기 참 쉽다.
여기서 위와 같이 출력될 수 있었던 이유는 eat 라는 함수를 "dog"를 통해서 실행했기 때문이다. 즉 실행 시점에서 this가 dog로 결정되는 것이다.
약간 아리송할 때 다음의 코드를 살펴보자. 이제 dog.eat라는 함수를 전역 레벨의 변수에 할당시켜 두고 호출해본다.
let eatFun = dog.eat;
eatFun();
eatFun이 전역 레벨이기 때문에 마지막 줄은 window.eatFun()과 같다. (물론 let 변수는 windowr객체에 binding 되지 않기 때문에 window.eatFun()은 오류이다. 말이 그렇다는 것이다.)즉 이제는 window를 통해서 실행시키는 것이다. 출력 결과는 아래와 같다.
즉 이번에는 this가 window로 binding 되었기 때문에 this.species는 window.species가 되었고 당연히 undefined인 것이다.
이처럼 this는 동일한 함수라고 하더라도 누구를 통해서 호출되느냐(execution context)에 따라서 정체성이 바뀌어버리는 고약한 녀석이다.
this의 고정
그럼 this를 확인하려면 누구를 통해서 호출하는지만 알면 되니까 그래도 좀 쉽게 파악할 수 있지 않을까? 생각할 수 있지만 this는 apply/bind/call과 같은 함수를 통해서 고정할 수도 있다. 다음 코드 처럼 eatFun의 bind를 이용해 dog를 this로 binding 시켜보자.
let bindToDog = dog.eat.bind(dog);
bindToDog();
이제는 분명 window를 통해서 실행하고 있음에도 불구하고 출력 결과 this는 dog이다.
물론 전혀 새로운 객체를 만들어서 binding 해도 잘 동작할 것이다.
let cat = {
species: "스핑크스",
};
let bindToCat = dog.eat.bind(cat);
// 아래 함수만 가지고 this를 추정할 수 있을까?
bindToCat();
여전히 하나의 함수인 dog.eat를 사용하고 있지만 이제 cat까지 잘 먹일 수 있게 되었다.
콜백함수에서의 this
콜백 함수는 어떤 일을 처리할 때전달된 함수(콜백 함수)를 이용해서 처리하기 위한 함수이다.
예를 들어 배열을 정렬할 때 사용되는 sort 함수는 파라미터로compareFn 타입의 콜백함수를 받는다. 그럼 그 내부에서 사용되는 this는 누구일까?
콜백 함수는 전역함수로 실행되기 때문에 기본적으로 콜백 내부의 this는 일반적으로 window가 된다. 전역 객체가 정렬 기능을 수행하면서 콜백을 불러주기 때문이다.
let arr = [1, 2, 3];
arr.sort(function (a, b) {
console.log("sort의 this", this); // this는 window
return a - b;
});
let timer = {
alarm: function (timeout) {
console.log("alarm function 의 this", this);
setTimeout(function () {
console.log("settimeout의 this", this);
}, timeout * 1000);
},
};
timer.alarm(4);
하지만 모든 콜백에서 this가 window는 아니다. 예를 들어 이벤트 콜백은 이벤트 소스에 추가해준다. 따라서 event callback 내에서의 this는 이벤트 소스가 된다.
document.querySelector("#click").addEventListener("click", function (e) {
console.log(this);
console.log(e.currentTarget);
});
참고로 event source는 이벤트 객체가 가지는 currentTarget 속성으로도 얻을 수 있다.
생성자 함수에서의 this
다음은 생성자 함수에서 this에 대해 알아보자. 생성자 함수에서의 this는 함수를 통해서 생성된 객체를 나타낸다.
function Calculator() {
this.count = 20;
this.plus = function () {
this.count++;
console.log(`count: ${this.count}`, this);
};
}
let myCalc = new Calculator();
myCalc.plus();
Calculator를 생성자 함수로 만든 myCalc를 이용해서 plus()를 호출해보면 그때의 this는 myCalc가 된다. 따라서 plus도 정상적으로 동작하고 마지막에 출력한 myCalc.count는 21이 된다.
헤깔리는 this의 최대 대책
this와 관련된 문제는 bindToCat()이라는 함수만 가지고 this를 추정할 수 있을까?하는 항목이다. 어떤 함수를 전달받았을 때 그 함수가 binding 된 함수인지 그냥 함수인지 사용할 때는 알 수가 없다.
콜백 함수 중 event callback에서 this가 window가 아닌 event source라고 쉽게 생각할 수 있을까? 콜백이 최종적으로 어디에 등록되었는지 소스코드를 확인하지 못한 상황에서 이를 추정하기도 쉽지 않다.
따라서 개인적으로 this를 사용하는 가장 확실한 방법중 하나는 잘 모르겠을 때 꼭 콘솔에 this를 출력해보고 이녀석이 지금 누구인지 확인하고 사용하자라는 것이다.(너무 성의 없나 ㅜㅜ)