본문은 자바스크립트에서 이벤트 핸들러의 내부 this가 무엇을 바인딩하는지에 대해 설명한다.
이벤트 핸들러 어트리뷰트 값으로 할당한 함수는 이벤트 핸들러에 의해 일반 함수로 호출한다. 일반 함수로서 호출되는 함수 내부의 this는 전역 객체 window를 가리키므로, 함수 내부의 this는 window를 가리킨다.
<!DOCTYPE html>
<html>
<head>
<body>
<button onclick="handlerClick()">click me!</button>
<script>
function handlerClick() {
console.log(this); // window
}
</script>
</body>
</html>
단 이벤트 핸들러를 호출할 때 인수로 전달한 this는 이벤트를 바인딩한 DOM 요소를 가리킨다.
<!DOCTYPE html>
<html>
<head>
<body>
<button onclick="handlerClick(this)">click me!</button>
<script>
function handlerClick(button) {
console.log(button); // 이벤트를 바인딩한 button 요소
console.log(this); // window
}
</script>
</body>
</html>
이벤트 핸들러 프로퍼티 방식과 addEventListener 메서드 방식은 모두 이벤트 핸들러 내부의 this는 이벤트를 바인딩한 DOM 요소를 가리킨다. 즉 이벤트 핸들러 내부의 this는 이벤트 객체의 currentTarget 프로퍼티와 같다.
<!DOCTYPE html>
<html>
<head>
<body>
<button class="btn1">0</button>
<button class="btn2">0</button>
<script>
const $btn1 = document.querySelector('.btn1');
const $btn2 = document.querySelector('.btn2');
// 이벤트 핸들러 프로퍼티 방식
$btn1.onclick = function(e) {
console.log(this); // $btn1
console.log(e.currentTarget); // $btn1
console.log(this === e.currentTarget); // true
++this.textContent;
}
// 이벤트 리스너 메서드 방식
$btn2.addEventListener('click', function(e) {
console.log(this); // $btn2
console.log(e.currentTarget); // $btn2
console.log(this === e.currentTarget); // true
++this.textContent;
});
</script>
</body>
</html>
화살표 함수로 정의한 이벤트 핸들러 내부 this는 상위 스코프 this를 가리킨다.
클래스에서 이벤트 핸들러를 바인딩하는 경우 this에 주의해야 한다.
<!DOCTYPE html>
<html>
<head>
<body>
<button class="btn">0</button>
<script>
class App {
constructor() {
this.$button = document.querySelector('.btn');
this.count = 0;
// increase 메서드를 이벤트 핸들러로 등록
this.$button.onclick = this.increase;
}
increase() {
// 이벤트 핸들러 increase 내부의 this는 DOM 요소(this.$button)을 가리킨다.
// 따라서 this.$button은 this.$button.$button과 같다.
this.$button.textContent = ++this.count;
// TypeError: Cannot set properties of undefined (setting 'textContent')"
}
}
new App();
</script>
</body>
</html>
위 예제는 increase 메서드 내부의 this는 클래스가 생성할 인스턴스를 가리키지 않는다. 이벤트 핸들러 내부의 this는 이벤트를 바인딩한 DOM 요소를 가리키므로 increase 메서드 내부의 this는 $this.button을 가리킨다. 따라서 increase 메서드를 이벤트 핸들러로 바인딩할 때 bind 메서드를 사용해 this를 전달하여 increase 메서드 내부의 this가 클래스를 생성할 인스턴스를 가리키도록 하자.
<!DOCTYPE html>
<html>
<head>
<body>
<button class="btn">0</button>
<script>
class App {
constructor() {
this.$button = document.querySelector('.btn');
this.count = 0;
// increase 메서드 내부의 this가 인스턴스를 가리키도록 bind
this.$button.onclick = this.increase.bind(this);
}
increase() {
this.$button.textContent = ++this.count;
}
}
new App();
</script>
</body>
</html>
또는 클래스 필드에 할당한 화살표 함수를 이벤트 핸들러로 등록하여 이벤트 핸들러 내부의 this가 인스턴스를 가리키도록 할 수 있다. 이 때 이벤트 핸들러 increase는 프로토타입 메서드가 아닌 인스턴스 메서드가 된다. 즉 increase는 인스턴스 메서드이므로 내부 this는 클래스를 생성할 인스턴스를 가리키는 것이다.
<!DOCTYPE html>
<html>
<head>
<body>
<button class="btn">0</button>
<script>
class App {
constructor() {
this.$button = document.querySelector('.btn');
this.count = 0;
// 화살표 함수인 increase를 이벤트 핸들러로 등록
this.$button.onclick = this.increase);
}
// 클래스 필드 정의
// increase는 인스턴스 메서드이므로 내부 this는 인스턴스를 가리킨다.
increase = () => this.$button.textContent = ++this.count;
}
new App();
</script>
</body>
</html>
이벤트 핸들러 프로퍼티 방식과 addEventListener 메서드 방식의 경우 이벤트 핸들러 내부에서 함수를 호출하면서 인수를 전달할 수 있다.
<!DOCTYPE html>
<html>
<head>
<body>
<lable>User Name <input type="text"/></lable>
<em class="message"></em>
<script>
const MIN_USER_NAME_LENGTH = 5;
const $input = document.querySelector('input[type=text]');
const $msg = document.querySelector('.message');
const checkUserNameLength = min => {
$msg.textContent = $input.value.length < min ? `이름은 ${min}자 이상으로 입력해 주세요` : '';
}
// 이벤트 핸들러 내부에서 함수를 호출하면서 인수를 전달한다.
$input.onblur = () => {
checkUserNameLength(MIN_USER_NAME_LENGTH);
}
</script>
</body>
</html>