자바스크립트를 공부하면서 정말 어려운 개념을 손꼽자면
this
를 빼놓을 수 없다. 언제는window
객체를 내놓았다가 언제는 상위 객체를 내놓았다가 하다보니 혼란이 온다. 특히, 콜백 함수로 전달되는 경우에는 예상치 못한undefined
를 만나고 당황하기도 했다. 이걸 이겨내지 못하면 보통의 개발자도 되지 못하겠지... 어서this
를 뿌셔보자
자바스크립트에서 this는 기본적으로 실행 컨텍스트가 생성될 때 함께 결정된다. 실행 컨텍스트는 함수를 호출할 때 생성되므로, 바꿔 말하면 this는 함수를 호출할 때 결정된다고 할 수 있다.
전역 공간에서 this
는 전역 객체를 가리킨다. 브라우저 환경에서 전역객체는 window
이고 Node.js 환경에서는 global
입니다.
프로그래밍 언어에서 함수와 메서드는 미리 정의한 동장을 수행하는 코드 뭉치로, 둘의 차이는 독립성에 있다. 함수는 자체로 독립적인 기능을 수행하는 반면, 메서드는 자신을 호출한 객체에 관한 동작을 수행한다.
기억해야하는 점은 어떤 함수가 객체의 프로퍼티에 할당된다고 해서 무조건 메서드가 되는 것이 아니라 객체의 메서드로서 호출할 경우에만 메서드로 동작하고, 그렇지 않으면 함수로 동작한다.
let obj = {
method: function(x) {console.log(this, x); }
};
obj.method(1); // {method: f} 1
obj['method'](2); // {method: f} 2
함수로서의 호출과 메서드로서의 호출을 구분하는 방법은 함수 앞에 점(.)
이 있는지 확인하면 된다. 점 표기법이든 대괄호 표기법이든, 어떤 함수를 호출하는 경우에 함수 이름 앞에 객체가 명시돼 있는 경우라면 메서드이다.
this
에는 호출한 주체에 대한 정보가 담긴다. 어떤 함수를 메서드로 호출하는 경우 호출 주체는 바로 함수명(프로퍼티명) 앞의 객체이다.
this
는 호출한 주체의 정보가 담긴다고 했다. 그런데 함수로서 호출하는 것은 호출 주체가 따로 없고 개발자가 직접 관여해서 실행한 것이기 때문에 호출 주체의 정보를 알 수 없다. 따라서, 전역 객체를 바라보게 된다.
this
바인딩에 대해서 함수를 실행하는 당시의 주변 환경(메서드 내부인지, 함수 내부인지) 등은 중요하지 않다. 오직 해당 함수를 호출하는 구문 앞에 점 또는 대괄호 표기가 있는지 없는지가 관건이다. 예시를 보자.
let obj = {
outer: function () {
console.log(this); // (1)
let innerFunc = function() {
console.log(this); // (2) (3)
};
innerFunc();
let obj2 = {
innerMethod: innerFunc
};
obj2.innerMethod();
}
};
obj.outer();
위의 경우를 보면 obj1, window, obj2
가 나온다. innerFunc
의 경우 동일한 함수이지만 함수로서 호출됐는지, 아니면 앞에 객체가 붙어 호출됐는지에 따라 결과가 달라졌다.
this
가 호출 주체가 없을 때는 자동으로 전역객체를 바인딩하지 않고 주변 환경의 this
를 그대로 상속받아 사용할 수 있다면 좋을 것이다. 이를 우회하는 방법으로 변수를 활용할 수 있다.
let obj = {
outer: function() {
console.log(this); // (1) {outer: f }
let innerFunc1 = function() {
console.log(this); // (2) window {...}
};
innerFunc1();
let self = this;
let innerFunc2 = function(){
console.log(self); // (3) {outer: f}
};
innerFunc2();
}
};
obj.outer();
outer 스코프에서 self라는 변수에 this
를 할당하고 이를 이용해서 출력하도록 하면 객체 obj가 출력된다.
기존의 this
의 경우 함수 내부에서 전역 객체를 바라보는 문제가 있었다. 이를 보완하기 위해서 ES6에서는 this
를 바인딩하지 않는 함수인 arrow function
을 도입했다. 화살표 함수의 경우 실행 컨텍스트를 생성할 때 this
바인딩 과정이 빠져서 상위 스코프의 this
를 그대로 활용할 수 있다.
let obj = {
outer: function() {
console.log(this); // (1) {outer: f}
let innerFunc = () => {
console.log(this); // (2) {outer: f}
};
innerFunc();
}
};
obj.outer();
innerFunc
의 경우에 함수로 호출했음에도 불구하고 this
바인딩 과정이 발생하지 않아 상위 스코프인 outer의 this
를 그대로 사용했다.
document.body.innerHTML += `<button id='a'>클릭</button>`;
document.body.querrySelector("#a")
.addEventListener("click", function(e) {
console.log(this, e);
}));
addEventListener
는 지정한 엘리먼트에 "click" 이벤트가 발생할 때마다 그 이벤트 정보를 콜백 함수의 첫 번째 인자로 삼아 함수를 실행하라는 명령이다. 버튼을 클릭하면 앞서 지정한 엘리먼트와 클릭 이벤트에 관한 정보가 담긴 객체가 출력된다.
여기서 addEventListener
는 콜백함수를 호출할 때 자신의 this
를 상속하도록 정의돼 있다. 따라서 메서드명의 점(.)
앞부분이 곧 this
가 된다.
이처럼 콜백함수에서의 this
는 단순하게 무엇이다라고 확답을 내리기 어렵다. 콜백함수의 제어권을 가진 함수가 this
를 무엇으로 할지 결정한다. 만약 아무런 정의도 발생하지 않는다면 여전히 기본적인 함수와 마찬가지로 전역객체를 바라본다.
let Cat = function(name, age) {
this.bark = '야옹';
this.name = name;
this.age = age;
};
let choco = new Cat('초코', 7);
let nabi = new Cat('나비', 5);
console.log(choco, nabi);
// Cat {bark: '야옹', name: '초코', age: 7}
// Cat {bark: '야옹', name: '나비', age: 5}
자바스크립트는 함수에 생성자로서 역할을 함께 부여했다. new
키워드를 통해서 함수를 호출하면 해당 함수가 생성자로서 동작하고, 내부의 this
는 곧 새로 만들 인스턴스 자신이 된다.
마치며!✨
정리를 하면서도 자꾸만 '어렵다' 라는 생각이 들고 있는this
에 관해서 정리했다. 다음으로는 사용하는 환경에 따라서 변경되는this
를 바인딩 하는 방법에 대해서 다뤄보겠다.