자바스크립트의 this
자바스크립트에서 함수가 호출되면, 매개변수로 전달되는 인자값 외에도 arguments 객체와 this 를 전달받는다. 여기서 자바스크립트에서의 this 는 다른 언어의 this 와는 약간 다르게 동작하기 때문에 주의해야한다. 또한 Strict Mode와 Sloppy Mode에서도 약간이 차이가 있다.
예를 들어, Java에서의 this 는 인스턴스 자신(self)를 가리키는 참조 변수로 사용된다. this 가 객체 자신에 대한 참조 값을 가지고 있다는 뜻이다. 주로 매개변수와 객체 자신이 가지고 있는 멤버 변수명이 같을 경우 이를 구분하기 위해서 사용된다. 하지만 자바스크립트의 경우 Java와는 다르게 this 에 바인딩되는 객체는 한 가지가 아니다. 해당 함수의 호출 방식에 따라 this 에 바인딩되는 객체가 달라진다!!
Global Context
전역 문맥에서의 this 는 Strict Mode 여부에 관계 없이 전역 객체(Global Object)를 참조한다.
전역객체(Global Object)는 모든 객체의 유일한 최상위 객체를 의미하며 일반적으로 Browser-side에서는 window, Server-side(Node.js)에서는 global 객체를 의미한다.
const num = 10;
console.log(this === window);
console.log(window.num);
console.log(num);
> true
> 10
> 10
함수 호출 방식
함수 내부에서 this 의 값은 함수를 호출한 방법에 의해 달라지게 된다. 자바스크립트는 함수를 선언할 때 this 에 바인딩할 객체가 정적으로 결정되지 않고, 함수를 호출할 때 함수가 어떻게 호출되었는지에 따라 this 에 바인딩할 객체가 동적으로 결정된다는 말이다. 함수를 호출하는 방식은 아래와 같다.
- 함수 호출
- 메소드 호출
- 생성자 함수 호출
- apply / call / bind 호출
함수 호출
일반적인 함수 호출(function 사용)에서 this 는 전역객체에 바인딩된다.
function f1() {
console.log("f1's this: ", this); // Window
function f2() {
console.log("f2's this: ", this); // Window
}
f2();
}
f1();
하지만 위와 마찬가지로 전역객체인 window를 통해 변수를 가져올 수 있다.
const num = 10;
function f1() {
console.log("f1's this: ", this.num);
function f2() {
console.log("f2's this: ", this.num);
}
f2();
}
f1();
> f1's this: 10
> f2's this: 10
this.num을 통해 값을 할당해줘도 함수의 this 는 전역객체에 바인딩된다.
const num = 10;
function f1() {
this.num = 20;
console.log("f1's this: ", this);
console.log("f1's num: ", this.num);
function f2() {
this.num = 30;
console.log("f2's this: ", this.num);
}
f2();
}
f1();
> f1's this: Window
> f1's num: 20
> f2's this: 30
메소드의 내부 함수와 콜백 함수의 경우에도 위와 동일하다. 내부함수는 일반 함수, 메소드, 콜백함수 어디에서 선언되었든 관게없이 this 는 전역객체를 바인딩한다.
더글라스 크락포드는 “이것은 설계 단계의 결함으로 메소드가 내부함수를 사용하여 자신의 작업을 돕게 할 수 없다는 것을 의미한다” 라고 말한다.
내부 함수의 this 가 전역 객체를 참조하려는 것을 방지하려면 아래와 같은 방법을 사용하거나 apply, call, bind 메소드를 사용해야한다.
const obj = {
value: 100,
f1: function () {
const that = this;
console.log("f1's this: ", this); // obj
console.log("f1's this.value: ", this.value);
function f2() {
console.log("f2's this: ", this); // window
console.log("f2's this.value: ", this.value);
console.log("f2's that: ", that); // obj
console.log("f2's that.value: ", that.value);
}
f2();
},
};
obj.f1();
> f1's this: {value: 100, f1: ƒ}
> f1's this.value: 100
> f2's this: Window {0: Window, 1: Window, window: Window, self: Window, document: document, name: '', location: Location, …}
> f2's this.value: undefined
> f2's that: {value: 100, f1: ƒ}
> f2's that.value: 100
메소드 호출
함수가 객체의 프로퍼티 값이면 메소드로서 호출된다. 이때 메소드 내부의 this 는 해당 메소드를 소유한 객체, 즉 해당 메소드를 호출한 객체에 바인딩된다.
var obj1 = {
name: 'Lee',
sayName: function() {
console.log(this.name);
}
}
var obj2 = {
name: 'Kim'
}
obj2.sayName = obj1.sayName;
obj1.sayName();
obj2.sayName();
또한, 프로토타입 객체도 메소드를 가질 수 있다. 프로토타입 객체 메소드 내부에서 사용된 this 도 일반 메소드 방식과 마찬가지로 해당 메소드를 호출한 객체에 바인딩된다.
function Person(name) {
this.name = name;
}
Person.prototype.getName = function() {
return this.name;
}
var me = new Person('Lee');
console.log(me.getName());
Person.prototype.name = 'Kim';
console.log(Person.prototype.getName());
생성자 함수 호출
자바스크립트의 생성자 함수는 말 그대로 객체를 생성하는 역할을 한다. 다른 객체 지향 언어와는 다르게 기존 함수에 new 연산자를 붙여 호출하면 해당 함수는 생성자 함수로 동작하는 형식이다. 이때 주의해서 봐야할 점!! new 연산자와 함께 생성자 함수를 호출하면 this 바인딩이 메소드나 함수 호출 때와는 다르게 동작한다. 이 때 this 는 새로 생긴 객체에 묶이게 된다.
function Person(name) {
this.name = name;
}
var me = new Person('Lee');
console.log(me.name);
this를 통해 멤버 변수를 넣어놓은 생성자 함수를 new 연산자없이 호출하면 해당 함수의 this 는 전역객체에 바인딩된다. this가 전역 객체에 바인딩되고 멤버 변수는 전역객체에 종속되게 된다.
생성자 함수를 new 없이 호출한 경우, 함수 Person 내부의 this 는 전역객체를 가리키므로 name은 전역변수(window)에 바인딩된다. 또한 new와 함께 생성자 함수를 호출하는 경우에 암묵적으로 반환하던 this 도 반환하지 않으며, 반환문이 없으므로 undefined를 반환하게 된다.
일반함수와 생성자 함수에 특별한 형식적 차이는 없기 때문에 일반적으로 생성자 함수명은 첫문자를 대문자로 기술하여 혼란을 방지하려는 노력을 한다. 그러나 이러한 규칙을 사용한다 하더라도 실수는 발생할 수 있다.
이러한 위험성을 회피하기 위해 사용되는 패턴(Scope-Safe Constructor)은 다음과 같다. 이 패턴은 대부분의 라이브러리에서 광범위하게 사용된다. 대부분의 빌트인 생성자(Object, Regex, Array 등)는 new 연산자와 함께 호출되었는지를 확인한 후 적절한 값을 반환한다.
apply / call / bind 호출
Function.prototype.apply, Function.prototype.call 메소드를 이용하면 자바스크립트 엔진의 암묵적 this 바인딩이 아닌 this 를 특정 객체에 명시적으로 바인딩할 수 있다.
apply
구문은 아래와 같다.
func.apply(thisArg, [argsArray])
- thisArg는 func 를 호출하는데 제공될 this 의 값을 의미한다.
- argsArray는 func 이 호출되어야 하는 인수를 지정하는 유사 배열 객체을 의미하며 함수에 제공된 인수가 없을 경우 null 또는 undefined로 설정된다.
const numbers = [5, 6, 2, 3, 7];
const max = Math.max.apply(null, numbers);
const min = Math.min.apply(null, numbers);
console.log("최대값:", max, "최소값:", min);
> 최대값: 7 최소값: 2
const f1 = function(num) {
this.num = 10;
};
const f2 = {};
f1.apply(f2, ['num']);
console.log(f2);
> {num: 10}
call
구문!!
func.apply(thisArg[, arg1[, arg2[, ...]]])
- thisArg는 apply와 마찬가지로 func 를 호출하는데 제공될 this 의 값을 의미한다.
- arg1, arg2, .... 는 apply가 배열로 값을 넘기는 것과는 call은 다르게 하나의 인자를 각각 넘긴다.
func.apply(myfunc, [1, 2, 3]);
func.call(myfunc, 1, 2, 3);
apply()와 call() 메소드는 콜백 함수의 this를 위해서 사용되기도 한다.
function NumFunc(num) {
this.num = num;
}
NumFunc.prototype.myMethod = function (callback) {
if (typeof callback == 'function') {
callback.call(this);
}
};
function myfunc() {
console.log(this.num);
}
var p = new NumFunc(10);
p.myMethod(myfunc);
> 10
bind
bind는 ES5에 추가되었다. 구문은 call과 비슷하다.
func.bind(thisArg[, arg1[, arg2[, ...]]])
인자와 함께 곧바로 함수를 호출하는 call, apply와 달리 bind는 메소드를 사용한 함수와 똑같은 생김새의 함수를 반환한다!!
const module = {
num: 10,
getNum: function(num) {
return this.num + num;
}
};
const unboundGetNum = module.getNum;
console.log(unboundGetNum(10));
const boundGetNum = unboundGetNum.bind(module);
console.log(boundGetNum(10));
> NaN
> 20
참고자료
함수 호출 방식에 의해 결정되는 this: https://poiemaweb.com/js-this
MDN - this: https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Operators/this
MDN - apply: https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Function/apply
MDN - call: https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Function/call
MDN - bind: https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Function/bind