우선 왜 공부할까?
프로그래머스 과제를 하면서 this를 너무 남발하는 느낌이 들었다. 또한 this가 정확히 무엇을 가리키고 있는지 거의 일일히 console.log를 찍어봤는데 예측 가능한 프로그래밍을 해보잣 👊👊
객체는 상태를 나타내는 프로퍼티와 동작을 나타내는 메서드를 하나의 논리적 단위로 묶은 복합적인 자료구조다. 동작을 나타내는 메서드는 자신이 속한 객체의 상태, 즉 프로퍼티를 참조하고 변경할 수 있어야 한다. 이 때 메서드가 자신이 속한 객체의 프로퍼티를 참조 하려면 먼저 자신이 속한 객체를 가리키는 식별자를 참조할 수 있어야 한다. 객체 리터럴 방식으로 생성한 객체의 경우 메서드 내부에서 메서드 자신이 속한 객체를 가리키는 식별자를 재귀적으로 참조할 수 있다.
const circle = {
radius : 5, //프로퍼티: 객체 고유의 상태 데이터
getDiameter(){ // 메서드 : 상태 데이터를 참조하고 조작하는 동작
return 2 * circle.radius // 자신이 속한 객체인 circle을 참조할 수 있어야 한다
}
}
console.log(circle.getDiameter()) //10
getDiameter 메서드 내에서 자신이 속한 객체를 가리키는 식별자 circle을 참조하고 있다. 이 참조 표현식이 평가되는 시점은 getDiameter 메서드가 호출되어 함수 몸체가 실행되는 시점이다.
위 예제의 객체 리터럴은 circle 변수가 할당되기 직전에 평가된다. 따라서 getDiameter 메서드가 호출되는 시점에는 이미 객체 리터럴의 평가가 완료되어 객체가 생성되었고 circle 식별자에 생성된 객체가 할당된 이후다. 따라서 메서드 내부에서 circle 식별자를 참조할 수 있다.
function Circle(radius) {
???.radius = radius; /// 이 시점에는 생성자 함수 자신이 생성할 인스턴스를 가리키는 식별자를 알 수 없다.
}
Circle.prototype.getDiameter = function() {
return 2 * ????.radius // 이 시점에는 생성자 함수 자신이 생성할 인스턴스를 가리키는 식별자를 알 수 없다.
}
const circle = new Circle(5) // 생성자 함수로 인스턴스를 생성하려면 먼저 생성자 함수를 정의해야 한다.
생성자 함수 내부에서는 프로퍼티 또는 메서드를 추가하기 위해 자신이 생성할 인스턴스를 참조할 수 있어야 한다. 하지만 생성자 함수에 의한 객체 생성 방식은 먼저 생성자 함수를 정의한 이후 new 연산자와 함께 생성자 함수를 호출하는 단계가 추가적으로 필요하다. 다시 말해, 생성자 함수로 인스턴스를 생성하려면 먼저 생성자 함수가 존재해야 한다.
🎉 this는 자신이 속한 객체 또는 자신이 생성할 인스턴스를 가리키는 자기참조변수다. this를 통해 자신이 속한 객체 또는 자신이 생성할 인스턴스의 프로퍼티나 메서드를 참조할 수 있다.
this는 자바스크립트 엔진에 의해서 암묵적으로 생성, 어디서든 참조할 수 있다. 함수를 호출하면 arguments 객체와 this가 암묵적으로 함수 내부에 전달된다. 단, this가 가리키는 값, 🤸🏻 즉 this 바인딩은 함수 호출 방식에 의해 동적으로 결정된다.
this 바인딩
바인딩이란 식별자와 값을 연결하는 과정을 의미. 예를 들어, 변수 선언은 변수 이름(식별자)과 확보된 메모리 공간의 주소를 바인딩 하는 것이다. this 바인딩은 this(키워드로 분류되지만 식별자 역할)와 this가 가리킬 객체를 바인딩하는 것이다.
function Circle(radius) {
this.radius = radius; //this는 생성자 함수가 생성할 인스턴스를 가리킨다.
}
Circle.prototype.getDiameter = function() {
return 2 * this.radius //this는 생성자 함수가 생성할 인스턴스를 가리킨다.
}
const circle = new Circle(5) //인스턴스 생성
console.log(circle.getDiameter()) //10
객체 리터럴의 메서드 내부에서의 this는 메서드를 호출한 객체, 즉 circle을 가리키며 생성자 함수 내부의 this는 생성자 함수가 생성할 인스턴스를 가리킨다. 이처럼 this는 상황에 따라 가리키는 대상이 다르다.
//this는 어디서든지 참조가 가능하다.
// 전역에서 this는 전역 객체 window를 가리킨다
console.log(this); // window
function square(num){
console.log(this); // window // 일반함수 내부에서 this는 전역 객체 window
return num * num
}
square(2)
const person = {
name : 'John',
getName : function() { //메서드 내부에서 this는 메서드를 호출한 객체를 가리킨다
console.log(this); // {name : "John", getName: f}
return this.name;
}
};
function Person(name){
this.name = name //생성자 함수 내부에서 this는 생성자 함수가 생성할 인스턴스를 가리킨다.
console.log(this) //Person { name: 'boram' }
}
const me = new Person('boram')
strict mode가 적용된 일반함수 내부의 this에는 undefinded가 바인딩 된다.
this 바인딩은 함수 호출방식. 즉 함수가 어떻게 호출되었는지에 따라 동적으로 결정된다.
📄 렉시컬 스코프와 this 바인딩은 결정시기가 다르다
함수의 상위 스코프를 결정하는 방식인 렉시컬 스코프는 함수 정의가 평가되어 함수 객체가 생성되는 시점에서 상위 스코프를 결정한다. 하지만 this 바인딩은 함수 호출 시점에 결정된다.
문제는 함수를 호출하는 방식이 다양하다는 것이다.
기본적으로 this 에는 전역 객체가 바인딩된다.
function foo(){
console.log("foo's this: ",this) //foo's this: <ref *1> Object [global] window
function bar(){
console.log("bar's this: ",this) //bar's this: <ref *1> Object [global] window
}
bar()
}
foo()
위 예제처럼 전역 함수는 물론 중첩 함수를 일반 함수로 호출하면 함수 내부의 this에는 전역 객체가 바인딩된다. 다만 this는 객체의 프로퍼티나 메서드를 참조하기 위한 자기 참조 변수이므로 객체를 생성하지 않는 일반함수의 this는 사실 의미가 없다. 콜백 함수가 일반 함수로 호출된다면 콜백 함수 내부의 this도 전역객체. 어떤 함수라도 일반 함수로 호출되면 this에 전역 객체가 바인딩 된다.
var value = 1
const obj ={
value: 100,
foo() {
console.log("foo's this: ", this) //foo's this: { value: 100, foo: [Function: foo] }
setTimeout(function () { //콜백함수 내부의 this에는 전역 객체가 바인딩
console.log("callback's this:", this) //window
console.log("callback's value:", this.value) //1
},100)
}
};
obj.foo()
이처럼 일반 함수로 호출된 모든 함수(중첩함수, 콜백함수 포함) 내부의 this에는 전역 객체가 바인딩된다. 그러나 위 예제의 경우 메서드 내부에서 setTimeout함수에 전달된 콜백 함수의 this에는 전역 객체가 바인딩 된다. 따라서 this.value는 obj 객체의 value 프로퍼티가 아닌 전역 객체의 value 프로퍼티, 즉 window.value를 참조하고 있는 것은 문제가 있다.(1) 메서드 내부의 중첩함수나 콜백 함수의 this 바인딩을 메서드의 this 바인딩과 일치시키기 위한 방법은 다음과 같다.
var value = 1;
const obj = {
value: 100,
foo() {
//this 바인딩을 변수 that에 할당한다
const that = this;
//콜백 함수 내부에서 this대신 that을 참조
setTimeout(function(){
console.log(that) //{ value: 100, foo: [Function: foo] }
console.log(that.value) //100
},100)
}
}
obj.foo()
위 방법 이외에도 자바스크트는 this를 명시적으로 바인딩할 수 있는 apply, call, bind 메서드를 제공한다. 또는 화살표 함수를 사용해서 this 바인딩을 일치시킬 수도 있다.
var value = 1;
const obj = {
value: 100,
foo() {
//콜백 함수 내부에서 this대신 that을 참조
setTimeout(()=> console.log(this.value),100) //100
}
}
obj.foo()
메서드 내부의 this에는 메서드를 호출한 객체, 즉 메서드를 호출할 때 메서드 이름 앞의 마침표(.) 연산자 앞에 기술한 객체가 바인딩된다. 주의할 것은 소유한 객체가 아니라 메서드를 호출한 객체에 바인딩 된다는 것.
const person = {
name = 'boram',
getName() {
return this.name; //메서드 내부의 this는 메서드를 호출한 객체에 바인딩
}
};
console.log(person.getName()); //getName을 호출한 객체는 person이다. //boram
위 예제의 getName 메서드는 person 객체의 메서드로 정의되었다. 메서드는 프로퍼티에 바인딩된 함수다. 즉, person 객체의 getName 프로퍼티가 가리키는 함수 객체는 person 객체에 포함된 것이 아니라 독립적으로 존재하는 별도의 객체다. getName 프로퍼티가 함수 객체를 가리키고 있을 뿐이다. 따라서 getName 프로퍼티가 가리키는 함수 객체, 즉 getName 메서드는 다른 객체의 프로퍼티에 할당하는 것으로 다른 객체의 메서드가 될 수도 있고 일반 변수에 할당하여 일반 함수로 호출될 수도 있다.
// getName 메서드를 anotherPerson 객체의 메서드로 할당
anotherPerson.getName = person.getName
// getName 메서드를 호출한 객체는 anotherPerson이다
console.log(anotherPerson.getName()) //kim
// getName 메서드를 변수에 할당
const getName = person.getName;
// getName 메서드를 일반 함수로 호출
console.log(getName()) // ' '
// 일반함수로 호출된 getName 함수 내부의 this.name은 브라우저 환경에서 window.name과 같다
브라우저 환경에서 window.name은 브라우저 창의 이름을 나ㅏ내는 빌트인 프로퍼티이며 기본값은 ''이다. Node.js 환경에서 this.name은 undifinded이다.
따라서 메서드 내부의 this는 프로퍼티로 메서드를 가리키고 있는 객체와는 관계가 없고 메서드를 호출한 객체에 바인딩된다. 프로토타입 메서드 내부에서 사용된 this도 일반 메서드와 마찬가지로 해당 메서드를 호출한 객체에 바인딩된다.
function Person(name) {
this.name = name;
}
Person.prototype.getName = function(){
return this.name;
};
const me = new Person('Lee');
// getName 메서드를 호출한 객체는 me이다.
console.log(me.getName()); //Lee`
Person.prototype.name = 'kim';
//getName 메서드를 호출한 객체는 Person.prototype이다.
console.log(Person.prototype.getName()); //kim
lee가 나온 경우 getName 메서드를 호출한 객체는 me다. 따라서 getName 메서드 내부의 this는 me를 가리키며 this.name은 'Lee'다. kim의 경우 getName 메서드를 호출한 객체는 Person.prototype이다. Person.prototype도 객체이므로 직접 메서드를 호출할 수 있다. 따라서 getName 메서드 내부의 this는 Person.prototype을 가리키며 this.name은 kim이다
생성자 함수 내부의 this에는 생성자 함수가 (미래에) 생성할 인스턴스가 바인딩된다.
function Circle(radius) {
this.radius = radius;
this.getDiameter = function () {
return this.radius * 2
};
}
const circle1 = new Circle(5);
const circle2 = new Circle(10);
console.log(circle1.getDiameter()); //10`
console.log(circle2.getDiameter()); //20`
생성자 함수는 이름 그래도 객체(인스턴스)를 생성하는 함수다. 일반함수와 동일한 방법으로 생성자 함수를 정의하고 new 연산자오 함께 호출하면 해당 함수는 생성자 함수로 동작한다. 만약 new 연산자와 함께 생성자 함수를 호출하지 않으면 생성자 함수가 아니라 일반함수로 동작한다.
const circle3 = Circle(15); // new 연산자와 함께 호출하지 않으면 생성자 함수로 동작하지 않는다.
console.log(circle3); //undefined 일반함수로 호출된 Circle에는 반환문이 없다 -> undefined
console.log(radius) // 15 일반함수로 호출된 Circle 내부의 this는 전역 객체
apply, call, bind 메서드는 Function.prototype의 메서드이다. 즉, 이들 메서드는 모든 함수가 상속받아 사용할 수 있다.
apply와 call 메서드의 본질적인 기능은 함수를 호출하는 것이다. apply와 call 함수를 호출하면서 첫 번째 인수로 전달한 특정 객체를 호출한 함수의 this에 바인딩 한다. apply와 call 메서드는 호출할 함수에 인수를 전달하는 방식만 다를 뿐 동일하게 동작한다. bind 메서드는 메서드의 this와 메서드 내부의 중첩함수 또는 콜백함수의 this가 불일치하는 문제를 해결하기 위해 유용하게 사용된다.
지금까지 함수 호출 방식에 따라 this바인딩이 동적으로 결정되는 것에 대해 살펴봤다. 이를 정리해보면 다음과 같다.
일반 함수 호출..... ➡️ 전역 객체
메서드 호출 ...........➡️ 매서드를 호출한 객체
생성자 함수 호출 ➡️ 생성자 함수가 (미래에) 생성할 인스턴스
Function.prototype.apply/call/bind 메서드에 의한 간접 호출
➡️ 메서드에 첫번째 인수로 전달한 객체
공부하고 느낀점
this에 대해서 한 번 쭉 정리를 할 수 있어 좋았다. 사실 this를 쓰면서도 이게 정확하게 어떤 것을 가리키고 있는지 헷갈릴 때가 많았는데. 그런데 예제에 쓰인 코드 정도의 복잡도가 아닌 더더더더 복잡한 코드들이 실제로는 쓰이기 때문에 의식하면서 마지막에 정리한 유형 중 어디에 속하는 this를 사용하고 있나 확인하면서 코딩을 해야겠다!