[JS] 모던 Javascript Deep Dive 22장 - this
업데이트:
생성자 함수에서부터 this를 많이 다뤘지만 계속 정확한 의미는 알지 못하고 넘어갔다.
this의 큰 특징은 어떻게 호출되었느냐에 따라 값이 결정된다는 것이다. 아직은 와닿지 않는데 무슨 말인지 차근히 알아보자 !
[01] this 키워드
this 키워드는 객체 리터럴 안에서도 쓰일 수 있고 생성자 함수 안에서도 쓰일 수 있다.
그런데 위 두가지 상황에서 가리키는 대상이 다르다.
어떻게 다른지 코드를 통해 확인해보자.
🎯 객체 리터럴 내부의 this
const circle = {
radius : 5,
getDiameter(){
return 2 * this.radius; // this 메서드를 호출한 객체 circle을 가리킴.
}
};
console.log(circle.getDiameter()); // 10
🎯 생성자 함수 내부의 this
fuction Circle(radius){
this.radius = radius; // 생성자 함수가 생성할 인스턴스를 가리킴
}
Circle.prototype.getDiameter = function() {
return 2 * this.radius; // 생성자 함수가 생성할 인스턴스를 가리킴.
};
const circle = new Circle(5); // 인스턴스 생성
console.log(circle.getDiameter()); // 10
두가지 경우의 this는 상황에 따라 가리키는 대상이 다른 것을 확인했다.
객체 리터럴 안의 메서드로 호출하면 호출한 객체를 가리키고 생성자 함수 안의 메서드로 호출하면 생성자 함수의 인스턴스를 가리킨다.
함수가 호출되는 방식에 따라 this 바인딩이 동적으로 결정된다는 것을 알 수 있다.
동적 스코프 this
이전에 13장 스코프 단원에서 렉시컬 스코프와 동적 스코프를 다룬적이 있었다.
자바스크립트는 기본적으로 렉시컬 스코프를 따르기 때문에 함수가 정의된 위치에서 함수의 상위 스코프를 결정한다.
그런데 this는 동적 스코프를 따르기 때문에 함수를 어디서 호출했는지에 따라 함수의 상위 스코프를 결정하는 것이다.
☝🏻 결국 렉시컬 스코프는 함수가 정의되면 스코프가 결정되고 this는 함수가 호출되면 스코프가 결정된다는 것이다 !
[02] 함수 호출 방식과 this 바인딩
함수를 호출하는 방식에도 여러가지가 있다.
- 일반 함수 호출
- 메서드 호출
- 생성자 함수 호출
- Function.prototype.apply/call/bind 메서드에 의한 간접 호출
위 방법 4가지에서 각 this는 어떤 값을 가질지 알아보자.
1) 일반 함수 호출
기본적으로 this에 전역 객체 window가 바인딩된다.
function generalThis(){
console.log(this);
}
generalThis(); // window
내부 함수에서의 this 호출
내부함수도 일반함수, 메소드, 콜백함수 어디에서 선언되었든 항상 this는 전역객체를 바인딩한다.
그런데 내부함수에서 this가 전역객체를 바인딩하는 것은 자바스크립트 설계 단계의 결함이라고 한다. 전역객체를 참조하는 것을 피하고 내부함수의 this가 외부함수의 this와 바인딩 될 수 있는 아래의 방법을 이용하는 것이 좋다.
var value = 1;
var obj = {
value: 100,
foo: function() {
var that = this; // this === obj
console.log("foo's this: ", this); // obj
console.log("foo's this.value: ", this.value); // 100
function bar() {
console.log("bar's this: ", this); // window
console.log("bar's this.value: ", this.value); // 1
console.log("bar's that: ", that); // obj
console.log("bar's that.value: ", that.value); // 100
}
bar();
}
};
obj.foo();
that변수에 obj 객체의 this를 할당한 뒤 that을 이용하면 내부함수가 전역객체를 참조하는 것을 피할 수 있다.
🏷 예외 사항 : strict mode
만약 생성자 함수인데 new 연산자를 까먹고 일반함수로 호출 시, this가 전역 객체를 바인딩해버려서 문제가 발생할 수 있다. 이러한 문제 때문에 ES5부터 strict mode에서는 this에 undefined를 바인딩 시켜준다.
⚠ strict mode일때는 일반함수를 호출하면 this에 undefined가 바인딩된다.
function strictThis(){
'use strict'
console.log(this);
}
strictThis(); // undefined
2) 메서드 호출
메서드로 호출하면 호출한 객체에 바인딩된다. 즉, 마침표 연산자 앞에 기술한 객체가 바인딩된다.
function Dog(name) {
this.name = name;
}
Dog.prototype.getName = function () {
return this.name;
};
const dog = new Dog("maru");
Dog.prototype.name = "ruby";
console.log(dog.getName()); // maru
// 호출한 객체 : dog
console.log(Dog.prototype.getName()); // ruby
// 호출한 객체 : Dog.prototype
3) 생성자 함수 호출
생성자 함수가 미래에 생성할 인스턴스가 바인딩된다.
// 생성자 함수
function Person(name) {
this.name = name;
}
var me = new Person('minhye');
console.log(me); // Person {name: "Lee"}
// new 연산자와 함께 생성자 함수를 호출하지 않으면 생성자 함수로 동작하지 않는다.
var you = Person('Kim');
console.log(you); // undefined
생성자 함수가 인스턴스를 생성하지 않으면 일반함수처럼 동작할 수 있다. 그래서 혼란 방지를 위해 생성자 함수의 함수명은 첫문자를 대문자로 한다.
4) apply/call/bind 메서드에 의한 간접 호출
apply, call, bind 메서드는 Function.prototype의 메서드다.
🤷♀️ 왜 사용할까 ??
함수 내 this가 다른 사람의 작업으로 인해 의도치 않게 바뀔수도 있기 때문에 이를 방지하기 위해 this를 지정해 주어야 할 때 위 3가지 메서드를 사용하는 것이다.
apply(), call() 메서드
apply와 call 메서드는 둘 다 this를 설정하고 함수를 즉시 실행한다.
this로 사용할 객체와 인수 리스트를 인수로 전달받아 함수를 호출한다.
//@param은 변수를 설명할때 사용하고 @return은 반환문을 설명할 때 사용함.
/**
@param thisArg : this로 사용할 객체
@param argsArray : 함수에게 전달할 인수 리스트의 배열 또는 유사 배열 객체
@returns : 호출된 함수의 반환값
*/
Function.prototype.apply(thisArg[, argsArray])
/**
@param thisArg - this로 사용할 객체
@param arg1, arg2, ... - 함수에게 전달할 인수 리스트
@returns 호출된 함수의 반환값
*/
Function.prototype.call(thisArg[, arg1[, arg2[, ...]]])
apply와 call 메서드의 인수값의 뜻도 알았으니 직접 사용해보겠다.
function getThisBinding(){
console.log(arguments)
return this;
}
const thisArg = { a:1 };
console.log(getThisBinding());
console.log(getThisBinding.apply(thisArg, [1,2,3]));
//Arguments(3) [1,2,3, callee: f, Symbol(Symbol.iterator):f]
//{a:1}
console.log(getThisBinding.call(thisArg, 1, 2, 3));
//Arguments(3) [1,2,3, callee: f, Symbol(Symbol.iterator):f]
//{a:1}
위 두 메서드는 함수에게 리스트로 전달하는지 배열로 전달하는지 인수 전달 방식에만 차이가 있다.
bind() 메서드
bind 메서드는 메서드의 this와 메서드 내부의 중첩 함수 this가 불일치하는 문제를 해결하게 위해 사용된다.
위에서 다뤘던 변수 that을 이용하는 방식 대신 bind() 메서드를 쓰는것이 더 편리할 것으로 보인다!
const person = {
name: 'minhye',
foo(callback){
setTimeout(callback.bind(this), 5000);
}
};
person.foo(function(){
console.log(`Hi! my name is ${this.name}.`);
});
bind() 메서드를 이용하지 않았다면 person.foo의 내부함수에서 this가 window(전역객체)로 바인딩 되어 문제가 발생했을 것이다.
bind() 메서드로 callback 함수 내부의 this를 외부함수의 this와 일치시켜주어 문제를 해결했다.
맺음말
this 키워드가 어디서 호출되느냐에 따라 바인딩되는 객체가 어떻게 다른지 알아야 하는 파트였다.
가장 중요한 키워드인 this는 렉시컬 스코프가 아니라 동적 스코프를 따른다는 것을 명심하고 가면 좋을 것 같다 !
댓글남기기