JavaScript의 함수 - 생성자 함수에 this
라는 키워드가 굉장히 많이 나왔는데 이 this
는 뭘 하는 녀석일까?🙄
this
는 함수를 호출할 때 호출 방식에 따라 다르게 바인딩 되는 키워드이다.
백문이 불여일견, 코드부터 보면서 이해해보자.
var func = function(){
console.log(this);
};
func(); // node.js : global, browser : window
보시다시피, 일반적인 방법으로 함수를 호출하면 this
는 전역 객체에 바인딩 된다.
그렇다면 어떤 경우에 this
의 바인딩이 바뀌게 되는지 한 번 알아보자.
JavaScript의 함수 - 생성자 함수에서도 사용했던 예제를 다시 보자.
function Car(model, color){
this.model = model;
this.color = color;
}
var audi = new Car('S8L', 'black');
var genesis = new Car('G70', 'red');
console.log(audi); //Car { model: 'S8L', color: 'black' }
console.log(genesis); //Car { model: 'G70', color: 'red' }
new
연산자를 이용한 생성자 함수 호출 시 this
는 생성자 함수를 이용해 생성한 객체에 바인딩 된다.
객체 지향 언어인 java의 생성자를 이용한 초기화에서도 볼 수 있던 익숙한 코드다.
객체의 메서드를 호출 시 this
는 해당 메서드를 호출한 객체에 바인딩 된다. 아래의 코드를 한번 보자.
var cat = {
'sound' : 'meow',
'playSound' : function(){
console.log(this.sound);
}
};
var dog = {
'sound' : 'barkbark'
};
//dog 객체에 playSound 프로퍼티를 동적으로 생성하고, cat의 playSound 메서드를 참조
dog.playSound = cat.playSound;
cat.playSound(); // meow
dog.playSound(); // barkbark
cat
객체의 playSound
메서드는 this
의 sound
프로퍼티를 console
에 출력해주는 메서드다.
dog
객체에 동적으로 playSound
프로퍼티를 생성한 후, cat
의 playSound
메서드를 참조한 결과
각 객체에 대해 바인딩 된 this
가 서로 다른 것을 확인할 수 있다.
이러한 호출 방식에 따른 this
바인딩 때문에 가끔 원하지 않는 결과가 나올 때도 있다. 다음 예제를 한번 보자.
value = 0;
var obj = {
'value' : 1,
'outerFunc' : function(){
this.value += 1;
console.log(this.value); // 2
var innerFunc = function(){
// innerFunc의 this는 전역객체에 바인딩 된다.
this.value += 100;
console.log(this.value); // 100
};
// outerFunc 메서드의 내부함수 innerFunc 호출
innerFunc();
}
}
// obj객체의 메서드 outerFunc 호출
obj.outerFunc();
의도했던 결과는 obj객체의 value가 102가 되게 만드는 것이었다.
하지만 결과는 obj객체의 value는 2가 되었고, 엉뚱한 전역객체의 value에 100이 더해졌다.😥
이것은 호출 방식에 따른 this
바인딩의 결과인데, 내부함수 역시 함수이므로 일반 함수 호출 시 this
는 전역 객체에 바인딩 된다는 규칙에 따라 전역 객체에 100이 더해지게 된 것이다.
this
를 별도의 변수에 저장해서 이런 의도하지 않은 작동을 방지하는 방법도 존재한다. 확인해보자.
value = 0;
var obj = {
'value' : 1,
'outerFunc' : function(){
var that = this;
that.value += 1;
console.log(that.value); // 2
var innerFunc = function(){
//innerFunc의 this는 더이상 전역객체에 바인딩되지 않는다.
that.value += 100;
console.log(that.value); // 102
};
// outerFunc 메서드의 내부함수 innerFunc 호출
innerFunc();
}
}
// obj객체의 메서드 outerFunc 호출
obj.outerFunc();
outerFunc 메서드의 내부에 that = this
구문을 추가해줬다.
이는 내부 함수의 상위 함수에 바인딩 된 this
를 별도의 변수에 저장한 뒤, 해당 변수에 접근하는 방법이다.
물론 이 방식은 ES6에서 새로 생긴 Arrow Function
의 등장으로 위의 방법은 권장되지 않지만 일단 알아두자.
위의 주의할 점에서 봤듯, 함수 호출 방식에 따라 달라지는 this
는 원하지 않는 결과를 만들 수도 있다. 이렇게 함수의 호출 방식에 따라 this
바인딩이 바뀌는 것이 아니라, 원하는 객체에 this
를 바인딩하는 방법은 없을까?🤔
이 궁금증을 해결해 줄 메서드가 바로 Function.prototype
의 apply, call, bind
메서드다.
Function.prototype
은 모든 함수의 부모 역할을 하는 프로토타입 객체이다. 따라서 모든 함수가 해당 메서드를 사용할 수 있다는 말이다. 그러면 이제 위의 메서드에 대해 살펴보도록 하자!😆
apply() 메서드의 매개변수는 다음과 같다.
func.apply(thisArg, [argsArray])
thisArg는 this
로 바인딩 될 객체를 명시해주고, [argsArray]
는 func
함수 호출 시 전달 인자를 배열 형태로 전달한다.
어떻게 동작하는지 다음 코드를 통해 알아보자.
function printOwnProperty(){
for(prop in this){
console.log(prop + " : " + this[prop]);
}
};
var Animal = {
'name' : 'bbokbbok',
'age' : 16,
'sex' : 'Castrated Male',
'breed' : 'Yorkshire Terrier',
'species' : 'Canine'
};
printOwnProperty.apply(Animal);
/*
* name : bbokbbok
* age : 16
* sex : Castrated Male
* breed : Yorkshire Terrier
* species : Canine
*/
(apply()
는 어디까지나 해당 함수를 호출하는 것은 변하지 않고, this
바인딩만 바뀐다는 것을 이해하자.)
printOwnProperty()
는 this
로 바인딩 된 객체의 프로퍼티들을 모두 출력해주는 함수이다.
위에서 일반 함수 호출 시 this
는 전역 객체에 바인딩 된다고 이미 알고 있다. 하지만 출력된 내용을 보면 printOwnProperty.apply()
의 전달 인자로 전달한 Animal
객체의 프로퍼티를 출력해 주는 것을 볼 수 있는데, 이것이 apply()
를 이용한 명시적 바인딩이다.
함수의 프로퍼티 - arguments에서 설명했던 유사 배열 객체는 배열 메서드를 사용할 수 없다고 설명했다.
하지만 apply()
나 call()
메서드를 이용하면, 유사 배열 객체도 모든 배열 객체의 부모인 Array.prototype
이 가지고 있는 다양한 배열 메서드의 사용을 가능하게 한다.
call()
메서드의 매개변수는 다음과 같다.
func.call(thisArg[, arg1[, arg2[, ...]]])
apply()
메서드와 큰 차이는 없고, 단지 전달 인자를 리스트의 형태로 받아온다는 것밖에 없다.
apply()
와 call()
의 차이를 아래 코드를 보며 알아보자.
var Person = function(name, gender, age){
this.name = name;
this.gender = gender;
this.age = age;
return this;
};
// 빈 객체
var emptyObj = {};
// { name: 'Johnson', gender: 'male', age: 15 }
console.log(Person.apply(emptyObj, ['Johnson', 'male', 15]));
// { name: 'amanda', gender: 'female', age: 20 }
console.log(Person.call(emptyObj, 'amanda', 'female', 20));
이처럼 call()
과 apply()
는 전달 인자를 어떻게 넘겨주는가? 의 차이만 있다고 생각하자.
또한 apply()
와 call()
은 메서드 호출 시 해당 함수를 바로 호출한다. 그렇다면 bind()
와의 차이는 뭘까?
bind()
메서드의 매개변수는 다음과 같다.
func.bind(thisArg[, arg1[, arg2[, ...]]])
위의 두 메서드와 bind()
메서드의 차이는, bind()
메서드는 호출 시 this
를 첫 번째 전달 인자에 바인딩시킨 bound
함수를 반환한다.
역시 눈으로 보면 이해가 빠르다. 어떻게 사용하는지 확인해보자!😁
var Cat = function(sex, name){
this.sex = sex;
this.name = name;
return this;
};
var emptyObj = {};
var bound = Cat.bind(emptyObj);
console.log(bound('Castrated Male', 'kitty')); // { sex: 'Castrated Male', name: 'kitty' }
bind()
메서드를 호출하면 Cat
함수를 바로 호출하는 것이 아닌, bound
라는 변수에 대입하고, bound
를 이용해 다시 호출하는 것을 볼 수 있다.
그리고 아래처럼 bind()
또한 call()
처럼 전달 인자를 리스트 형식으로 전달 할 수 있다.
var Cat = function(sex, name){
this.sex = sex;
this.name = name;
return this;
};
var emptyObj = {};
var bound = Cat.bind(emptyObj, 'Castrated Male', 'kitty');
console.log(bound()); // { sex: 'Castrated Male', name: 'kitty' }
같은 결과가 나오는 것을 확인할 수 있다.
또, bind()
메서드는 전달 인자를 부분적으로 미리 지정하는 것도 가능하다. 아래를 참고하자.
var Cat = function(sex, name){
this.sex = sex;
this.name = name;
return this;
};
var emptyObj = {};
var bound = Cat.bind(emptyObj, 'Castrated Male');
console.log(bound('kitty')); // { sex: 'Castrated Male', name: 'kitty' }
console.log(bound('nero')); // { sex: 'Castrated Male', name: 'nero' }
console.log(bound('kitty', 'nero')); // { sex: 'Castrated Male', name: 'kitty' }
console.log(bound()); // { sex: 'Castrated Male', name: undefined }
sex
프로퍼티를 미리 지정해놓고, name
프로퍼티만 따로 전달해주는 모습이다.
보시다시피 함수의 기본 특성처럼 초과된 전달 인자의 경우 무시하고, 부족한 전달 인자의 경우는 undefined
로 처리되는 것을 알 수 있다.
참고 자료
MDN Web Docs - Function.prototype.apply()
MDN Web Docs - Function.prototype.call()
MDN Web Docs - Function.prototype.bind()