this
이 포스팅에서는 this 키워드에 대해 알아본다.
this는 자기참조변수로, 자신이 속한 객체나 자신이 생성할 인스턴스를 가리킨다.
자바스크립트 엔진에 의해 암묵적으로 생성되며, 어디서든 참조할 수 있다.
※ this의 바인딩은 함수호출방식에 따라 동적으로 결정된다. ★x100개다.
여기서 바인딩은 식별자와 값을 연결하는 과정을 의미하는데,
this 바인딩은 식별자이자 키워드인 this에 this가 가리킬 객체를 연결한다는 의미이다.
this가 함수 호출 시점에 결정되므로, 개발자가 작성한 코드와 다르게 동작할 우려가 있다.
그러므로 this는 동적으로 결정된다. 는 사실을 꼭 기억하고 this가 필요한 시점에 동작을 예상하며 코드를 작성해야 한다.
this는 기본적으로 전역 객체(브라우저의 경우 window, 노드의 경우 global)가 바인딩된다.
메서드에서 this는 메서드를 호출한 객체를 가리키며, 생성자 함수에서는 생성자 함수를 통해 생성한 인스턴스를 가리킨다.
이정도 지식을 우선 알아두고 시작해보자.
this는 함수의 호출 방식에 따라 동적으로 결정된다.
함수의 호출 방식은 다양하다.
1.일반 함수로 정의하여 호출한다.
2.메서드로 정의하여 호출한다.
3.생성자 함수로서 호출한다.
4.함수를 호출해주는 빌트인 함수의 프로토타입 메서드를 이용해 간접적으로 호출한다.
1번부터 4번까지 살펴보겠다.
1.일반 함수로 정의하여 호출한다.
function func(){
console.log(this);
function func2(){
console.log(this);
}
func2(); // window
}
func(); // window
일반함수로 호출하면, this에는 전역 객체가 바인딩된다.
당신이 어디서 내부함수로서의 함수를 선언했던간에, 항상 this는 전역객체이다.
대표적인 예시로 전역함수, 메서드 내부 함수, 콜백함수 등의 사용처가 있겠다.
그렇다면 객체의 메서드와 메서드 내부 함수의 this는 어떻게 다를까?
var name = 'global var';
const obj = {
name:'Tom',
say(){
console.log('my name is ', this.name);
function say2(){
console.log('my name is ', this.name);
}
say2();
}
};
obj.say();
// my name is Tom
// my name is global var
우선, var 키워드로 선언한 전역 변수는 전역 객체의 프로퍼티가 된다.
그러므로 window.name 으로 전역 객체의 프로퍼티인 var를 호출할 수 있다.
그래서 obj.say 메서드의 2개 this 중에 하나라도 전역객체를 가리킨다면, var가 출력 될 것이다.
obj 객체에는 메서드로 say가 있고, say는 this.name을 출력하고 내부에 중첩함수 say2를 가지고 있으며 say2에서도 this.name을 출력한다.
코드 결과로는 첫번째 this.name은 'Tom'을 출력하고 두번째 this.name은 'global var'를 출력한다.
say는 객체의 메서드로 자신이 속한 객체 obj를 참조하여 'Tom'을 출력하게 되고, say2 함수는 일반함수로 this로 전역객체를 참조하는 것이다.
다음과 같이 메서드 say의 this는 obj를 가리키지만, 메서드 내에 중첩함수인 say2의 this가 obj를 가리키지 않는다면 문제가 발생할 수 있다. 보통 메서드 내에 정의한 함수들은, 헬퍼함수로서 역할을 하기 위해 정의하기 때문이다. 그러므로 say 메서드가 해야하는 일을 나눠 가져야 한다.
이제 메서드에 존재하는 일반함수들의 this를 특정해줘야 한다.
var name = 'global var';
const obj = {
name:'Tom',
say(){
console.log('my name is ', this.name);
const that = this;
function say2(){
console.log('my name is ', that.name);
}
say2();
}
};
obj.say();
// my name is Tom
// my name is Tom
이와 같이 메서드의 this를 that 키워드에 할당하여 내부함수에서도 메서드의 this를 가리키도록 할 수 있다.
다른 방법으로는, 화살표 함수를 이용해도 된다.
var name = 'global var';
const obj = {
name:'Tom',
say(){
console.log('my name is ', this.name);
const say2 = ()=> {
console.log('my name is ', this.name);
}
say2();
}
};
obj.say();
// my name is Tom
// my name is Tom
화살표 함수의 this는 상위 스코프의 this를 가리킨다. 그러므로 상위 스코프 say 함수의 this인 obj를 가리킨다.
또 다른 방법으로는, 빌트인 객체 프로토타입의 메서드들을 이용할 수도 있다.
메서드에는 apply, call, bind가 있다.
var name = 'global var';
const obj = {
name:'Tom',
say(){
console.log('my name is ', this.name);
function say2(){
console.log('my name is ', this.name);
}
say2.apply(this);
say2.call(this);
}
};
obj.say();
// my name is Tom
// my name is Tom
// my name is Tom
이와 같이 빌트인 객체 Function의 프로토타입 메서드 apply, call, bind로 this를 특정시켜줄 수 있다.
이 메서드들은 뒤에서 자세히 다룰 예정이다.
2. 메서드로 정의하여 호출한다.
함수가 객체 프로퍼티의 값일수도 있다. 이 때는 메서드로서 호출된다.
메서드의 this에는 메서드를 호출한 객체가 바인딩된다.
즉, obj.method 라면, 메서드를 호출한 객체인 obj가 method 내부의 this 바인딩 대상객체가 된다.
const obj = {
name:'Tom',
say(){
console.log('my name is ', this.name);
}
};
obj.say(); // my name is Tom
그래서 say 메서드를 호출한 obj가 this가 되어 this.name => Tom을 출력하는 모습이다.
그런데 여기서 유의할 점이 있는데, this는 동적으로 결정된다는 점이다.
현재 코드에서는 마치 say 메서드에서의 this가 이미 obj로 정해져 있는 것 처럼 보인다.
그러나 this는 메서드가 호출될 때 정해지며, obj 객체의 메서드로 호출되어 obj 객체를 가리키고 있었을 뿐이다.
const obj = {
name:'Tom',
say(){
console.log('my name is ', this.name);
}
};
obj.say(); // my name is Tom
const obj2 = {
name:'James',
};
obj2.__proto__.say = obj.say;
obj2.say(); // my name is James
obj1이 가진 say 메서드를 obj2의 프로토타입 메서드에 추가했다.
obj2.say()를 실행하니 this가 obj2로 지정되어 James가 출력된 모습을 보인다.
이를 통해 다음을 알 수 있다.
say 메서드는 obj 객체에 종속된 함수가 아니다.
obj 객체라는 공간이 있다면, say 프로퍼티가 있고, 그 프로퍼티가 어떤 공간을 가리킨다.
say 프로퍼티가 가리키는 공간에 함수가 정의되어 있을 것이며, say 함수의 구현을 담고 있다.
그러므로 obj2.__proto__.say = obj.say 를 통해서, obj2의 프로토타입에 say 변수를 선언하고 obj 객체의 say 프로퍼티가 가리키는 공간에 대한 참조를 동일하게 가리키도록 한 것이다.
프로토타입에 정의된 say 메서드는 obj2.say()로 호출 될 것이고, 호출시 동적으로 this가 바인딩되기 때문에 obj2 객체가 바인딩된다.
결국 메서드가 객체 내부에 종속된 것이 아니므로, 다음과 같은 동작도 가능하다.
var name = 'hello world';
const obj = {
name:'Tom',
say(){
console.log('my name is ', this.name);
}
};
const say = obj.say;
say(); // my name is hello world
여기서 say는 this에 window가 바인딩되어 hello world를 출력한다.
다양한 예시를 통해 this가 바인딩 될 대상이 호출 시점에 결정된다는 사실이 얼마나 중요한지를 알게 되었다.
3. 생성자 함수로서 호출한다.
생성자 함수에서 선언된 this에는 생성자 함수를 통해 생성된 인스턴스가 바인딩된다.
자바스크립트에서 생성자 함수는 함수를 new 키워드로 호출하면 된다.
function Person(name){
this.name = name;
this.say = function(){
console.log('my name is ', this.name);
};
}
const tom = new Person('Tom');
tom.say(); // my name is Tom
const noTom = Person('Tom');
console.log(noTom); // undefined (리턴이 없어 undefined가 반환되었다.)
다음과 같이 new 키워드와 함께 생성자 함수로서 사용하면, this가 생성한 인스턴스에 바인딩된다.
new 키워드의 결과로서 암묵적인 return this;를 실행하고 tom에는 this 객체(인스턴스)가 담겨있다.
이 인스턴스는, 생성자 함수가 빈 객체를 생성하고 this에 바인딩하여 name, say 프로퍼티들을 담아 반환한 객체다.
new 키워드가 없는 단순 함수 호출에서는 함수의 return이 존재하지 않아서 return undefined;로 해석이 된다.
그래서 noTom에는 undefined 값이 담기게 된다.
그래서 단순호출의 경우에 undefined만 반환되고 아무런 부수효과는 없는걸까?
Person 함수 내의 로직은 this에 name, say 프로퍼티를 생성하는 것이다.
new 키워드 없이 Person 함수를 실행하면 this가 전역 객체를 가리킬 것인데, 결국 전역 객체에 name, say 프로퍼티를 생성한 것이다.
그래서 console.log(name); 을 찍어보면 window.name 을 참조하여 Tom을 반환할 것임을 예상할 수 있다.
4.함수를 호출해주는 빌트인 함수의 프로토타입 메서드를 이용해 간접적으로 호출한다.
Function.prototype.apply, call, bind 메서드를 이용하면 this 바인딩을 실현할 수 있다.
apply, call 메서드는 동작이 거의 비슷하나 매개변수를 받는 방식이 조금 다르다.
두 함수는 모두 '함수 호출'을 위한 메서드이다.
함수를 호출하면서, this에 바인딩 될 객체를 지정할 수 있다.
첫번째 인수로는 this로 지정할 객체를 넣어주고, 함수를 실행할 때 필요한 인수들을 두번째부터 넣어주면 된다.
var fire = 'fail..';
const thisObj = { fire : 'fire!' };
function func(a,b,c){
console.log(a,b,c, this.fire);
}
console.log(func(3,2,1)); // 3 2 1 fail..
console.log(func.apply(thisObj, [3,2,1])); // 3 2 1 fire!
console.log(func.call(thisObj, 3, 2, 1)); // 3 2 1 fire!
func(3,2,1)로 호출하면 this가 window를 가리키므로 3 2 1 fail.. 이 출력된다.
func.apply와 call로 this에 thisObj 객체를 바인딩한다. 그리고 두번째 인자부터 필요한 인수를 넣어준다.
apply 메서드에는 인수를 배열 []로 묶어서 전달하면 되고,
call 메서드에는 인수를 쉼표로 구분해서 전달하면 된다.
bind 메서드는 조금 다르게 동작한다.
bind 메서드는 this를 바인딩해주는 기능은 동일하나, this가 바인딩 된 함수 자체를 리턴한다.
그러므로 리턴된 함수를 다른 변수에 할당해서 사용하거나, 콜백으로서 전달할 수 있다.
var fire = 'fail..';
const thisObj = { fire : 'fire!' };
function func(a,b,c){
console.log(a,b,c, this.fire);
}
const bindFunc = func.bind(thisObj);
func(3, 2, 1); // 3 2 1 fail..
bindFunc(3, 2, 1); // 3 2 1 fire!
const bindFunc2 = func.bind(thisObj, 100);
bindFunc2(2, 1); // 100 2 1 fire!
apply, call 함수와 동일하게 첫번째 인자로 this에 바인딩할 객체를 받으며, 두번째 인자부터는 함수 자체에서 받는 인자의 값을 미리 설정할 수 있다.
다음과 같이 func.bind의 결과를 bindFunc에 할당해서 사용할 수 있으며 a 인자 값을 100으로 설정하여 사용할 수 있다.
실제로 코딩하면서 코드가 의도대로 동작하지 않는 경우에 이 this가 원인인 경우가 종종 있었다. 특히나 이벤트핸들러에 넣어주는 콜백과 setTimeout등의 타이머함수에서 사용하는 콜백 등에서 this가 원하는 대로 동작하지 않았던 기억이 난다. 이벤트, 타이머등은 비동기로 동작하기 때문에, 개발자 입장에서 더더욱 원인과 문제를 파악하기 어렵기도 하다. 그렇기 때문에 우리는 this가 결정되는 방식을 이해하고 화살표 함수, apply, call, bind 등의 여러 방법들을 이용해서 우리의 코드가 예상대로 동작하도록 만들어야 한다.
'JavaScript > theory' 카테고리의 다른 글
[자바스크립트] 마이크로 태스크 큐의 비동기 작업 처리와 렌더링 시점을 알아보자. (1) | 2022.04.14 |
---|---|
브라우저/노드 환경에서 모듈, AMD, CommonJS, UMD 알아보기 (1) | 2022.02.15 |
자바스크립트의 undefined, null에 대해 알아보자. (0) | 2022.01.19 |
자바스크립트 클래스란? (0) | 2022.01.18 |
객체 프로퍼티의 getter와 setter (0) | 2022.01.17 |