thenullpage.com
메서드를 변수에 한 번 옮겨 담았을 뿐인데 그 안의 this가 사라져 있었다. 함수 몸통은 한 글자도 안 고쳤는데 결과가 달라지는 게 납득이 안 돼서 한참 들여다봤다. this는 함수가 어디에 적혀 있는지가 아니라 그 함수를 누가 어떻게 불렀는지로 정해진다는 걸 그때 알았다.
같은 함수인데 부르는 방법에 따라 달라졌다. 두 줄을 나란히 찍어보면 바로 보인다.
const user = {
name: "김개발",
hello() {
console.log(this.name);
},
};
user.hello(); // 김개발
const fn = user.hello;
fn(); // undefined, 같은 함수인데 결과가 다르다
차이는 점 앞에 뭐가 있느냐뿐이다. user.hello()는 점 앞의 user가 this가 된다. fn()은 점 앞에 아무것도 없어서 this가 정해지지 않고, 요즘 모듈 환경에서는 undefined가 들어간다. 함수를 어디에 적었는지는 아무 상관이 없다.
콜백으로 넘기는 순간 그 점이 사라진다. 내가 당한 게 정확히 이거였다.
const timer = {
label: "저장",
run() {
console.log(this.label + " 실행");
},
};
timer.run(); // 저장 실행
setTimeout(timer.run, 100); // undefined 실행
timer.run이라고 적었으니 timer도 같이 딸려갈 것 같지만, 넘어가는 건 함수 하나뿐이다. 시간이 되면 setTimeout 안쪽에서 점 없이 불린다. 부르는 줄에서 점을 살려두면 해결된다.
setTimeout(() => timer.run(), 100); // 저장 실행
화살표 함수는 자기 this를 아예 안 만든다. 그래서 감싸는 용도로 쓸 만하다.
const counter = {
count: 0,
start() {
setInterval(function () {
this.count = this.count + 1; // 여기 this는 counter가 아니다
console.log(this.count); // NaN이 계속 찍힌다
}, 1000);
},
};
counter.start();
안쪽 function은 자기 this를 새로 갖는다. 타이머가 부른 거라 counter와는 연결이 끊긴다. undefined에 1을 더하니 NaN인데 에러가 안 나서 원인이 잘 안 보인다. 화살표로 바꾸면 끝난다.
start() {
setInterval(() => {
this.count = this.count + 1;
console.log(this.count); // 1, 2, 3
}, 1000);
}
화살표 함수는 this를 만들지 않고 자기가 적힌 자리의 this를 그대로 쓴다. 변수 못 찾으면 바깥으로 올라가는 것과 같은 방식이다. 이 화살표는 start 안에 적혀 있고 start는 counter.start()로 불렸으니 this는 counter다.
그렇다고 메서드 자리에 화살표를 쓰면 안 된다. 좋다길래 전부 바꿨다가 한 번 더 헤맸다.
const config = {
name: "설정",
show: () => {
console.log(this.name);
},
};
config.show(); // undefined, 점을 찍고 불렀는데도 안 된다
객체 중괄호는 스코프를 만들지 않는다. 화살표가 바깥으로 올라가 찾는 this는 config가 아니라 파일 맨 바깥의 this다. 점 앞의 객체는 절대 *** 않는다. 그래서 화살표는 남에게 넘길 콜백에만 쓴다.
bind는 this를 함수에 아예 붙여버린다. 넘길 함수를 미리 묶어두는 방법이다.
const bound = timer.run.bind(timer); // 실행이 아니라 새 함수를 돌려준다
setTimeout(bound, 100); // 저장 실행
const stolen = bound;
stolen(); // 저장 실행, 어떻게 옮겨 담아도 안 바뀐다
bind는 함수를 실행하지 않고 this가 고정된 새 함수를 만들어준다. 원본 timer.run은 그대로 남는다. 지금 당장 한 번만 다른 this로 부르고 싶으면 call과 apply가 있다.
function introduce(greeting) {
console.log(greeting + " " + this.name);
}
introduce.call(user, "안녕"); // 안녕 김개발
첫 번째 자리에 this로 쓸 값을 넣고 나머지 인자를 뒤에 이어 붙인다. 인자를 배열로 묶어 주는 apply도 있는데 차이는 그것뿐이라 call만 써도 된다.
리스너 안의 this는 리스너가 붙은 요소다. 규칙이 하나 더 있는 게 아니라 브라우저가 그 요소를 this로 정해서 불러주는 것이다.
const btn = document.querySelector("#save");
btn.addEventListener("click", function () {
console.log(this.id); // save
});
btn.addEventListener("click", (e) => {
console.log(this.id); // 여기 this는 요소가 아니다
console.log(e.currentTarget.id); // save
});
화살표를 쓰면 브라우저가 정해준 this를 안 받고 바깥 것을 그대로 쓴다. 요소가 필요하면 this 대신 e.currentTarget을 본다. 이쪽이 더 분명해서 나는 리스너에서 this를 아예 안 쓴다.
클래스 메서드를 그냥 넘겼다가 또 같은 함정에 빠졌다. 점이 사라지는 건 클래스라고 다르지 않다.
class Form {
count = 0;
submit() {
this.count = this.count + 1;
console.log(this.count);
}
}
const form = new Form();
form.submit(); // 1
btn.addEventListener("click", form.submit); // NaN, this가 버튼이라 count가 없다
고치는 길은 둘이다. 부를 때 점을 살리거나, 필드에 화살표로 선언해서 처음부터 붙여두거나.
btn.addEventListener("click", () => form.submit()); // 1, 2, 3
class Form2 {
count = 0;
submit = () => {
this.count = this.count + 1;
console.log(this.count); // 어디에 넘겨도 1, 2, 3
};
}
필드에 적은 화살표는 인스턴스가 만들어질 때 그 인스턴스를 물고 태어난다. 대신 인스턴스마다 함수가 하나씩 따로 생긴다.
new로 부르면 this는 그 자리에서 새로 만든 객체다. 이게 마지막 경우다.
function User(name) {
this.name = name;
}
const a = new User("이프론트");
console.log(a.name); // 이프론트
const b = User("이프론트"); // new를 빼먹으면
console.log(b); // undefined
new를 붙이면 빈 객체가 먼저 만들어지고 그게 this가 되어 돌아온다.
돌아보면 this는 순서가 정해진 짧은 목록이었다. new로 불렀나, bind나 call로 지정했나, 점 앞에 뭐가 있나, 셋 다 아니면 없다. 화살표 함수는 이 목록을 아예 타지 않고 바깥 것을 빌려 쓴다. 그래서 this가 이상하면 함수가 적힌 자리 말고 그 함수를 부르는 줄부터 찾는다. 거기서 점이 없어져 있으면 대체로 범인이었다.