this?
일반적인 프로그래밍 언어에서의 this는
객체지향언어에서 생성자 함수를 이용해 새로운 객체를 생성할 때 그 새롭게 생성된 객체를 가리키는 식별자입니다.
하지만 자바스크립트에서 this는 조금 더 다양한 맥락에서 사용할 수 있고, 다른 방식으로 동작합니다.
아래의 예제를 보겠습니다.
const foo = function(){
console.dir(this);
};
this가 어떤 객체를 가리키고 있는지를 출력해주는 간단한 함수입니다.
이 함수의 this는 해당 함수를 "어떻게" 호출하냐에 따라서 this가 가리키는 객체가 달라집니다.
// 일반 함수로 호출
foo(); // Window
// 일반 함수로 호출 시엔 전역 객체 window를 가리킨다.
// obj 객체의 메소드로 호출
const obj = { foo };
obj.foo(); // obj
// 객체의 메소드로 호출하면 메소드를 호출한 객체를 가리킨다.
// 생성자 함수로 호출
new foo(); //foo
// 생성자 함수로 호출하면 this는 자기 자신을 가리킨다.
foo는 모두 같은 함수인데, 호출하는 방식에 따라서 this가 가리키는 객체가 모두 달라집니다.
이러한 자바스크립트 this의 독특한 특질이 자바스크립트를 복잡하게 만드는 것 같습니다.
천천히 설명하면서 이해해보겠습니다.
우선, this는 객체를 가리키는 식별자입니다.
우리가 함수 안에서 this를 사용하게 되면 객체와 this가 연결, 즉 this가 어떠한 객체를 참조하게 되는데
이를 this 바인딩이라고 합니다. 이러한 this 바인딩은 렉시컬 스코프와는 다르게 함수 호출 방식에 따라서 동적으로 결정됩니다.
쉽게 말해서, 자바스크립트 내에 모든 함수는 this를 가지고 있고
이 this는 렉시컬 스코프처럼 선언과 동시에 this가 바인딩되지 않고
해당 함수가 호출된 맥락에 따라서 this 바인딩이 결정됩니다.
자바스크립트 엔진의 이러한 암묵적인 과정이 우리가 프로그램 안에서 this를 사용할 수 있게 해주는 것이지요.
위의 코드에서 보여줬던 간략한 맥락(바인딩 규칙)에 대해서 조금 더 자세히 설명해보겠습니다.
1. 일반 함수 호출 (기본 바인딩) - 전역 객체가 바인딩
function foo() {
console.log("foo's this : ", this); // this = window;
function bar() {
console.log("bar's this : ", this); // this = window;
}
bar();
}
foo();
전역 함수 foo와 foo의 중첩 함수 bar 모두 일반 함수로 호출 했을 시에는 전역 객체 window가 바인딩 됩니다.
* 일반 함수 : 생성자 함수, 객체의 함수 프로퍼티인 메서드가 아닌 일반적인 호출 함수
일반적으로 this는 객체의 프로퍼티나 메서드가 자기 자신을 참조하기 위한 참조 변수인데, 이러한 일반 함수에서의 this는 의미가 없습니다. 그래서 엄격 모드에서는 이러한 상황에서 전역 객체 window가 아닌 undefined가 반환됩니다.
function foo() {
'use strict'; // 엄격 모드
console.log("foo's this : ", this); // this = undefined;
function bar() {
console.log("bar's this : ", this); // this = undefined;
}
bar();
}
foo();
2. 메서드 호출 (암시적 바인딩) - 호출한 객체가 바인딩
const person = {
name: 'Lee',
getName: function(){
console.log(this.name); // Lee
}
};
person.getName(); // Lee
위 코드에서 person.getName()함수를 호출하면 getName()의 this는 해당 메서드를 호출한 객체를 가리키게 됩니다.
하지만 이런 메서드를 호출할 때 this 바인딩이 달라지는 몇 몇 경우가 있습니다.
그러한 패턴들을 알아보면서 this를 조금 더 깊게 이해해 보겠습니다.
const person = {
name: 'Lee',
getName: function(){
console.log(this.name); // Lee
}
};
person.getName(); // Lee
const newGetName = person.getName;
newGetName(); // ??
위의 코드를 살펴보겠습니다.
person.getName 함수를 새로운 변수 newGetName에 할당하였습니다.
이 때 newGetName()의 출력값은 어떻게 될까요?
똑같이 this에는 객체 person이 바인딩 되어있을까요?
우선 person 객체의 메서드를 살펴보겠습니다.
person 객체의 메서드 getName 함수는 getName 함수의 참조값을 가지고 있습니다.
따라서 getName함수를 새로운 변수에 할당하면 getName 함수의 참조값을 할당받게 되는 것입니다.
그래서 결론적으로는 getName 함수와 newGetName 함수는 똑같은 참조값을 가지고 있습니다.
하지만, 자바스크립트에서의 this는 함수를 호출하는 방식에 따라서 동적으로 바인딩된다고 하였습니다.
따라서 getName과 newGetName은 같은 함수를 가리키고 있지만,
newGetName()으로 전역 위치에서 함수를 호출하게 되면
자기 자신을 호출한 객체, 즉 window 객체를 가리키게 되는 것입니다.
전역 객체 window에는 name이라는 프로퍼티가 있고, 그 프로퍼티는 빈 문자열을 갖고 있기 때문에
빈 문자열이 출력됩니다.
즉 메서드 내부의 this는 해당 함수를 호출한 객체가 바인딩 됩니다.
3. 생성자 함수 호출 (new 바인딩) - 새롭게 생성될 객체가 바인딩
생성자 함수 내부의 this에는 생성자 함수가 미래에 생성할 인스턴스가 바인딩됩니다.
function Circle(radius){ // Circle 생성자 함수
this.radius = radius;
this.getDiameter = fucntion(){
return 2 * this.radius;
};
}
const circle1 = new Circle(5);
const circle2 = new Circle(10);
console.log(circle1.getDiameter()); // 10
console.log(circle2.getDiameter()); // 20
객체를 생성하는 생성자 함수를 new 연산자와 함께 호출하면 해당 생성자로 객체가 만들어집니다.
이 때 해당 함수 속에 this는 새롭게 생성될 객체(인스턴스) circle1, circle2가 this로 바인딩 됩니다.
4. call, apply, bind 메서드 (명시적 바인딩)
앞서 말했던 this 바인딩이 일어나는 상황이 아닌
다른 상황에 대해서 몇 가지 예를 들어보겠습니다.
메소드 내부의 중첩 함수의 this
var value = 1;
const obj = {
value:100,
foo(){
console.log(this.value); // 100
function bar(){
console.log(this.value); //1
}
bar();
}
};
obj.foo();
위의 예제에서는 obj 객체의 메서드 foo의 this는 객체 obj를 잘가리키지만,
foo 메서드의 중첩 함수인 bar의 this는 전역 객체를 가리키게 됩니다.
메서드를 콜백 함수로 전달
var value = 10;
const obj = {
value: 1,
getValue: function(){
console.log(this.value);
},
getValueResult: function(func){
var result = func();
console.log(result);
}
};
obj.getValue(); // 1;
obj.getValueResult(obj.getValue); // 10;
obj 메서드의 getValue 함수를 getValueResult 함수의 콜백 함수로써 전달했습니다.
이 경우에서도 this 바인딩이 의도와는 다르게 전역 객체를 바인딩하게 됩니다.
위 두가지 예는 공통적인 특징을 가지고 있습니다.
메서드 내부에서 중첩 함수를 호출하여 this를 사용하거나,
메서드 내부에서 콜백 함수를 호출하여 this를 사용하면,
일반 함수를 호출하는 것과 마찬가지로 this 바인딩이 상위 함수 스코프와는 다르게 끊어져버립니다.
이러한 현상은 함수 실행 컨텍스트로 설명할 수 있는데요.
오늘의 요점과는 다른 주제이기 때문에 다음에 설명하겠습니다.
위의 두가지 경우는 메서드 내부에서 외부 함수와 고차 함수를 보조해주는 헬퍼 함수이지만,
this가 다른 곳을 가르키는 문제를 발생하게 됩니다.
이러한 문제는 명시적 바인딩으로 해결할 수 있습니다.
var value = 1;
const obj = {
value:100,
foo(){
console.log(this.value); // 100
function bar(){
console.log(this.value);
}
bar.apply(obj); // 100
bar.call(obj); // 100
}
};
obj.foo();
바로 apply 함수와 call 함수를 사용하는 것 입니다.
apply 함수와 call 함수는 Function.prototype의 메서드이기 때문에 모든 함수들이 사용할 수 있습니다.
위의 코드,
bar.apply(obj);
bar.call(obj);
이 둘은 같은 의미를 갖고 있다. 의사적으로 설명하면
bar 함수를 호출하는데, 객체 obj를 this로 바인딩 해주세요!
라고 설명할 수 있습니다.
apply 함수와 call 함수는 형태는 갖지만 매개변수 타입을 다르게 받는 함수입니다.
함수 사용법에 대한 자세한 설명은 MDN에서 보세여 ㅎㅎ :)
이렇게 함수를 호출하면 모두 함수 내부에 this가 올바르게 obj를 가리키고 있는 것을 볼 수 있습니다.
이를 명시적 바인딩이라고 합니다.
또한 앞서 말한 콜백 함수에서의 this 연결 해제도 해결할 수 있습니다.
var value = 10;
const obj = {
value: 1,
getValue: function(){
console.log(this.value);
},
getValueResult: function(func){
var result = func.bind(obj)();
console.log(result);
}
};
obj.getValue(); // 1;
obj.getValueResult(obj.getValue); // 1;
위의 코드
var result = func().bind(obj)();
를 보시겠습니다.
bind 함수는 apply와 call과 비슷하게 this를 바인딩하는 함수인 것 같지만,
bind 함수는 함수를 호출하는 것이 아니라 함수를 반환해줍니다.
위의 코드를 의사적으로 설명한다면,
콜백 함수 func 내부의 this를 obj로 바꾼 함수를 반환해줘!!
라고 생각하시면 됩니다.
결과적으로 func.bind(obj)는 함수를 반환하기 때문에 뒤에 ()를 붙여 바로 호출해주는 것이지요!
이러한 bind 함수로 this 바인딩을 하드 바인딩이라고도 합니다.
이렇게 명시적 바인딩으로 메서드 내부의 this 바인딩을 일치시키는 작업을 해줄 수 있습니다.
아직 내공이 부족하여 설명이 불충하거나 예시가 적확하지 않을 수도 있습니다...
시간이 지나고 다시 읽어보고 부족한 부분이 있으면 수정이 필요할 것 같네요.
자유롭게 댓글 남겨주시면 참고하겠습니다. 감사합니다. :):)
※ 해당 게시글은 위키북스의 모던 자바스크립트 Deep Dive를 참조해서 작성하였습니다.
'WEB > JAVASCRIPT' 카테고리의 다른 글
Rest 파라미터, 스프레드 문법 (0) | 2023.04.10 |
---|---|
클로저 (0) | 2023.02.15 |
var, let, const (0) | 2023.02.07 |
스코프 (렉시컬 스코프, 스코프 체인 등 등) (0) | 2023.02.07 |
객체 리터럴 (0) | 2023.01.27 |