자바스크립트 내에서 this는 함수를 "호출"하는 방법에 의해 결정된다. 크게 4가지로 나뉜다.
기본적으로 this는 자바스크립트 실행 환경의 전역 객체인 Window를 가리킨다.
console.log(this); // Window
이는 strict mode(엄격 모드)에서도 마찬가지다.
'use strict';
console.log(this); // Window
함수 안에서 쓰인 this는 이 함수를 실행하는 주체에게 바인딩 된다.
function myFunction(){
return this;
}
console.log(myFunction()); // Window
여기서 myFunction은 전역 스코프에서 선언되었으므로 전역 객체에 등록되어있다. 즉 myFunction()은 window.myFunction()과 같다.
따라서 여기서의 this는 Window를 가리킨다.
엄격 모드에서는 함수 내 this에 디폴트 바인딩이 없기때문에 undefined가 된다.
'use strict';
function myFunction(){
return this;
}
console.log(myFunction()); // undefined
일반 함수가 아닌 어떠한 객체의 메서드를 호출하면 this는 해당 메서드를 호출한 객체가 된다.
const person={
name:'noma',
hello:function(){
return `Hello, ${this.name}!`;
},
};
person.hello(); // 'Hello, noma!'
즉, 여기서 this는 person 객체를 가리킨다.
const person={
name:'noma',
hello:function(){
return `Hello, ${this.name}!`;
},
};
const greeting=person.hello;
greeting(); // 'Hello, !'
앞서 말했듯, 전역 스코프에서 생성한 변수는 전역 객체에 등록된다.
greeting은 실질적으로 person 객체의 hello 메소드를 실행하지만, greeting이라는 전역 변수에 함수를 담은 것이기에 window.greeting()을 한 것과 같다. 따라서 여기서 this는 Window가 된다.
이벤트 핸들러에서 this는 이벤트를 받는 HTML 요소를 가리킨다.
const btn=document.querySelector('#btn');
btn.addEventListener('click',function(){
console.log(this); //#btn
});
위 함수는 콜백으로 등록되었고, 버튼에 클릭 이벤트가 발생되어야 실행된다. 즉 함수가 버튼에 의해 호출되므로 this는 버튼을 가리킨다.
함수(함수 객체)는 call, apply, bind 메소드를 가진다.이 함수들을 이용해서 this는 이걸로 바인딩 해줘!! 하고 명시할 수 있다.
function test() {
console.log(this);
}
const obj = { name: "noma" };
test.call(obj); // { name: 'noma' }
test.call("원시 네이티브 자료들은 wrapping 된다."); // [String: '원시 네이티브 자료들은 wrapping 된다.']
그냥 test를 호출했으면 this는 Window를 가리켰겠지만, call 함수로 obj를 전달함으로써 this는 obj를 가리키도록 고정되었다.
call, apply, bind 이 세 함수는 공통적으로 첫번째 인자로 넘겨주는 것을 this로 바인딩 하지만, 조금씩 차이가 있다.
function say(msg1,msg2){
console.log(`Hello, I'm ${this.name}.\n${msg1}\n${msg2}`);
}
const me={name:'noma'};
say.call(me,'Nice to meet you!','Bye💩');
say.apply(me,['Nice to meet you!','Bye💩']);
/*
Hello, I'm noma.
Nice to meet you!
Bye💩
*/
둘다 this를 바인딩하면서 함수를 호출한다. call과 apply의 유일한 차이점은, 첫 번째 인자(this를 대체할 값)를 제외하고 실제 함수를 실행시키는데 필요한 인자를 입력하는 방식에 있다. call()은 이를 하나씩 받고, apply()는 배열로 받는다.
bind는 call, apply와 달리 함수를 호출하지 않고, 인자로 받은 객체를 this로 하는 새로운 함수를 리턴한다. (두 번째 인자는 call과 같이 하나씩 받는다.)
say.bind(me,'Nice to meet you!','Bye💩')();
함수를 리턴하기때문에 바로 ()
를 이용해 호출할 수도 있다.
생성자 안에서 쓴 this는 생성자 함수가 생성하는 객체를 this로 가진다.
function Person(name){
this.name=name;
}
const her=new Person('noma');
console.log(her.name); // noma
new 바인딩이 동작하는 순서는 아래와 같다.
1. 새 객체가 만들어진다.
2. 새 객체의 prototype 체인이 호출 함수(생성자 함수)의 프로토타입과 연결된다.
3. 1.에서 만든 객체를 this로 사용하여 함수가 실행된다.
4. 이 함수가 객체를 반환하지 않는 한 1.에서 생성된 객체가 반환된다.
만약 new 키워드를 빼먹는 순간 일반 함수 호출과 같아지기 때문에, 이 경우 this는 Window가 된다.
const name="Hi, I'm Window!";
function Person(name){
this.name=name;
}
const her=Person('noma'); // her은 undefined이 됨
console.log(window.name); // 'noma'
여기까지 봤을 때 바인딩의 우선순위를 매겨보자면 대략 다음과 같다.
new 바인딩>=명시적 바인딩>암시적 바인딩>=기본 바인딩
new 바인딩은 실질적으로 새로운 객체를 생성하고 그 객체를 (명시적) 바인딩을 한 것이기때문에 new 바인딩>=명시적 바인딩이라고 하였고, 기본 바인딩은 암시적 바인딩의 일부여서 암시적 바인딩>=기본 바인딩이라고 표현하였다.
바인딩 케이스는 모두 알아봤지만 마지막으로 꼭 짚고 넘어가야하는 부분이 있다. 바로 화살표 함수이다.
화살표 함수는 자신의 this가 없다. 대신 화살표 함수가 둘러싸는 렉시컬 범위(lexical scope)의 this가 사용된다. 즉, 언제나 화살표 함수의 상위 스코프, 화살표 함수가 선언된 스코프의 this를 가리킨다.
아래 코드의 경우 내부 함수에서 this가 전역 객체인 Window를 가리키는 바람에 의도와는 다른 결과가 나왔다.
const Person=function(name){
this.name=name;
this.hello=function(){
console.log(this); // Person {name: 'noma', hello: ƒ}
setTimeout(function(){
console.log(this); // Window
console.log(`Hello, I'm ${this.name}!`); // Hello, I'm !
},1000);
};
};
const her=new Person('noma');
her.hello();
이럴 때 화살표 함수를 사용하도록 바꾸면 해당 화살표 함수의 상위 스코프의 this 즉, her 객체가 되므로 제대로 된 결과가 나오는 걸 볼 수 있다.
const Person=function(name){
this.name=name;
this.hello=function(){
console.log(this); // Person {name: 'noma', hello: ƒ}
setTimeout(()=>{
console.log(this); // Person {name: 'noma', hello: ƒ}
console.log(`Hello, I'm ${this.name}!`); // Hello, I'm noma!
},1000);
};
};
const her=new Person('noma');
her.hello();
화살표 함수가 나오기 전까지는, 모든 새로운 함수는 어떻게 그 함수가 호출되는지에 따라 자신의 this 값을 정의했다.
이는 객체지향 프로그래밍시 별로 좋지 않다. 아래 예제를 보자.
class Person{
constructor(name,age){
this.name=name;
this.age=age;
this.mouth=document.querySelector('.mouth');
this.mouth.addEventListener('click',this.onClick); //⛔
}
onClick(){
console.log(this);
alert(`Hi, I'm ${this.name}.`); //⛔
}
}
const me=new Person('noma',24);
// class가 mouth인 HTML요소를 클릭하면 'Hi, I'm undefined.'이 alert 되는 것을 확인할 수 있다.
함수 호출시 lexical environment라는 것이 생성되는 데 이 안에는 호출에 필요한 정보들이 함께 들어 있다. 그런데 함수 내부에서 this를 사용하는 클래스의 멤버 함수를 명시적 바인딩을 하지 않고 어딘가에 이 함수의 참조를 전달하게 되면 this가 해당 객체를 가지고 있지 않는 바인딩 이슈가 발생하게 된다.
따라서 멤버 함수를 다른 콜백으로 전달할 때 클래스 정보가 같이 전달되도록 하기 위해선 바인딩을 해줘야 한다. (멤버 함수 내에서 this를 사용하지 않으면 상관 없음)
여기서 방법은 크게 3가지다.
이는 arrow function 도입 이전에 쓰던 방법으로 비추천된다.
constructor(){
this.onClick=this.onClick.bind(this);
this.field.addEventListener('click',this.onClick);
}
위에서 언급했듯이 화살표 함수를 사용하면 해당 함수의 상위 스코프의 this가 바인딩된다고 했다. 여기서는 원하던 대로 me 객체를 this로 고정시킨다.
constructor(){
this.field.addEventListener('click',()=>this.onClick());
}
constructor(){
this.field.addEventListener('click',this.onClick);
}
onClick=()=>{
console.log(this);
alert(`Hi, I'm ${this.name}.`);
}
그러면 최종적으로 class가 mouth인 HTML요소를 클릭하면 'Hi, I'm noma.'라고 alert 되는 것을 확인할 수 있다.
물론 명시적으로 bind 해서 코딩하는 것을 좋아하는 사람도 있지만, arrow function은 이러한 바인딩 이슈를 개선하고 좀더 짧고 간단한 함수 표현을 위해 나온 ES6 문법인만큼 이를 사용하는 것을 추천한다.