this는 평소엔 딱히 어렵지 않다고 느껴지면서도, 오픈소스 등 남의 코드를 읽을 때에는 걸림돌이 되곤 한다. 이는 내가 this를 제대로 알지 못하기 때문인 걸로 느껴져서, 좀 더 공부해보기로 했다.
메서드 호출에서의 this
this는 현재 실행 컨텍스트이다. 아래 예시를 보자.
alert(this === window); // (1) 여기서 this는 window 객체, 즉 true
const caller = {
test: function() {
alert(this === window);
},
}
caller.test(); // (2) 여기서 this는 caller 객체, 즉 false- 참고로, (1)은 함수 호출, (2)는 메서드 호출이라고 한다. 함수와 메서드를 혼용해서 쓰는 경우도 있는데, 이처럼 엄밀히는 다르다.
- 하지만, 실제로 (1)의 케이스를 메서드 호출이라고 볼 수도 있다. 따지고 보면
window.alert(this === window);와 동일하기 때문이다. - 단, strict(엄격)모드에서는 이 둘을 다르게 보긴 한다. strict일 때에는 함수 호출로 하면
this가undefined가 된다.
이번엔 아래 예시를 보자.
const testObj = {
outerFunc: function() {
function innerFunc() {
console.log(this) // window
}
innerFunc()
},
}
testObj.outerFunc()- innerFunc가 호출될 때에는 아무 컨텍스트도 없다(바인드 되지 않았다). 따라서, 비엄격모드 기준으로
this는window가 되는 것이다. 물론 엄격모드에서는undefined일 것이다. - 반면, outerFunc의
this는 testObj이라는 객체가 되겠다.
생성자 함수 / 객체 내에서의 this
new키워드를 통해 만들어지거나 리터럴 표기에 의해 만들어진 객체에서,this는 해당 객체를 가리킨다.this는 보통 동적으로 결정되지만, 객체 내 메서드의this는 정적(Lexical)으로 결정되어 해당 메서드를 갖고 있는 객체가 된다.- 이는 다른 언어에서의 보편적인
this와 유사하므로 넘어가겠다.
이벤트 리스너에서의 this
- 이벤트 리스너에서
this는 이벤트를 발생시킨 객체이다.
컨텍스트가 없을 때 this를 정상적으로 쓰는 방법
객체 메서드가 객체 내부가 아닌 다른 곳에 전달되어 호출되면 this가 사라진다. 콜백함수를 쓰이는 경우, 클릭 이벤트 등으로 함수가 전달되는 등 케이스에서는 기존 this와의 연결이 끊기는 것이다. 뿐만 아니라, 함수 안에 함수가 존재하는 경우(앞선 예시에서 innerFunc)에는 this를 정상적으로 쓸 수 없었다. 어떻게 해결하면 될까?
this를 상수(const)로 저장해두기
- 간혹
this가 제대로 먹히지 않아, 어찌저찌this를 예컨대 self(물론 이 이름은 변경 가능)라고 두어 코드를 정상 작동시켰던 경우가 있었다. 그동안 난 원리를 잘 알지 못했으나, 이번에 알게 되었다. -
this를 상수로 지정했기 때문에 아래와 같이 서브루틴 안에서 이this를 찾을 수 있게 되는 것이다.(this가 freezing 되어버리는 느낌)function Family(firstName) { this.firstName = firstName const names = ['Seungwoo', 'Hyunsang', 'Zzanga'] const self = this names.map(function(value, index) { console.log(value + ' ' + self.firstName) }) } const hansFamily = new Family('Han') // Seungwoo Han // Hyunsang Han // Zzanga Han
bind 메서드 쓰기
- 매번 상수화하기는 좀 귀찮을 수 있다. 함수가 겹겹이 있을 수 있고, 그 때마다 상수의 이름을 정하는 것부터가 좀 그렇다.
-
따라서 조금 더 나은 방법으로,
bind메서드를 써서 서브루틴에 영구적으로this를 고정시킬 수 있다.function Family(firstName) { this.firstName = firstName const names = ['Seungwoo', 'Hyunsang', 'Zzanga'] names.map(function(value, index) { console.log(value + ' ' + this.firstName) }.bind(this)) } const hansFamily = new Family('Han') // Seungwoo Han // Hyunsang Han // Zzanga Han - 사실, React.js를 쓰면서 뭔지도 모르고
.bind(this)를 쓰고 있는 경우가 많을 것이다. 이번 기회에 알고 쓰자.
화살표함수 쓰기
- bind를 많은 사람들이 이해하기 어려워하기 때문에 화살표함수가 더 사랑받는 것도 있는 것 같다.(화살표함수는 곧 따로 다루어보도록 하겠다.)
- 일반적으로
this는 동적 스코프에 의해 결정되지만, 화살표함수를 썼을 때에는 자신을 둘러싼 정적(Lexical) 스코프에 의해this가 결정되게 된다. -
<모던 자바스크립트 튜토리얼>이라는 오픈소스 프로젝트의 한글판에 Contribution했을 때 이에 대해 다루면서 좀 더 알게 되었는데,
this를 bind하는 방법과 이렇게 화살표함수를 쓰는 방법은 엄밀히 말하면 조금 다르다고 한다. bind(this)를 했을 때this는 아예 한정이 되어버리는 것이고, 화살표함수 안에서의this는 마치 일반적인 변수가 상위 렉시컬 환경에서 변수의 검색이 이루어지는 것처럼, 상위 렉시컬 환경에서this를 찾게 되는 것이다.function Family(firstName) { this.firstName = firstName const names = ['Seungwoo', 'Hyunsang', 'Zzanga'] names.map((value, index) => { console.log(value + ' ' + this.firstName) }) } const hansFamily = new Family('Han') // Seungwoo Han // Hyunsang Han // Zzanga Han - 참고로 이 화살표함수가 포함된 코드를 babel로 트랜스파일링 해보면,
this를 const를 통해 저장해두는 것과 똑같이 변환됨을 알 수 있다. 그렇다, 화살표함수는 상위 렉시컬 환경에서this를 찾아 상수화하여 쓰는 방식으로 작동하는 것이다.(다시 말해, 앞서 소개한 ‘this를 상수로 저장해두기’와 ‘bind 메서드 쓰기’는 사실상 같은 거나 다름 없다는 것이다!)
[참고자료]
https://blueshw.github.io/2018/03/12/this/
https://ko.javascript.info/arrow-functions