Java와 같은 언어의 this는 그 쓰임새가 명확하고 간결하다고 할 수 있다. 근데 Javascript의 this는 어디서 어떻게 함수 호출에 쓰이느냐에 따라 가리키는 곳이 다양하다. 즉, 함수 호출방식에 따라서 this에 바인딩(묶일) 될 객체가 동적으로 결정된다. 그 부분에 대해서 정리를 해보려한다.
함수가 호출 되는 방식은 아래와 같은 방법이 있다.
- 함수 호출
- 메서드 호출
- 생성자 함수 호출
- apply, call, bind를 이용한 호출
1. 함수 호출
console.log(this) // window
function func() {
console.log(this) // window
function inner() {
console.log(this) // window
}
}
func()
기본적으로 this는 전역객체를 가리킨다. 함수 안에서 쓰여도 window, 내부 함수에서도 마찬가지이다. 함수 안에서 this가 window를 가리킨다기보다는 this가 가리키는(바인딩 된)곳이 바뀌지 않았다고 이해하면 더 쉬울 듯 하다.
setTimeout(function() {
console.log("callback's this: ", this); // window
console.log("callback's this.value: ", this.value); // 1
}, 100);
심지어 콜백함수 내에서도 this 는 전역객체를 가리킨다.
var value = 1;
var obj = {
value: 100,
func: function() {
console.log(this); // obj 이부분에 대해서는 뒤에서 얘기할 것이다.
console.log(this.value); // 100
function foo() {
console.log(this); // window
console.log(this.value); // 1
}
foo();
},
};
obj.func();
객체 obj의 메서드인 func의 내부함수에서도 this는 여전히 전역객체를 가리킨다. 이쯤되면 this === '전역객체'인가? 싶다.
위 상황에서 func 함수의 실행을 돕는 함수를 만들고 싶다면,
var value = 1;
var obj = {
value: 100,
func: function() {
console.log(this); // obj
console.log(this.value); // 100
this.func2("func!")
},
func2: function(name) {
console.log("I can help " + name);
}
};
obj.func();
위와 같이 해야한다. 이건 또 전역객체를 안가리키는 것 같다. 왜? 바로 뒷 주제에서 알아보겠다.
아래와 같은 트릭? 도있다. this 를 변수에 저장 해두는 것이다.
var value = 1;
var obj = {
value: 100,
func: function() {
var that = this;
console.log(this); // obj
console.log(this.value); // 100
function foo() {
console.log(that); // obj
console.log(that.value); // 100
}
foo();
},
};
obj.func();
2. 메서드 호출
자바스크립트에서 메서드는 두가지를 예로 들 수 있다.
1. 객체의 프로퍼티가 함수인 경우
2. 프로토 타입 객체에서 사용가능한 함수인 경우
객체의 프로퍼티가 함수인 경우
var obj1 = {
name: 'AnJungHwan',
sayName: function() { // 함수 메서드로 사용된 경우. 단 여기서 화살표 함수는 this가 전역객체를 가리킨다.
console.log(this.name);
}
}
var obj2 = {
name: 'ParkJiSung'
}
obj2.sayName = obj1.sayName;
obj1.sayName(); // AnJungHwan
obj2.sayName(); // ParkJiSung
두 객체에서 하나의 함수를 가리키고 있다. 그렇지만 호출 될때 메서드 내의 this 는 각각 다른 객체(obj1, obj2)를 가리킨다.
프로토타입 객체에서 사용가능한 함수의 경우
function Car(name) {
this.name = name;
}
Car.prototype.getName = function () {
return this.name
}
var sportsCar = new Car("Porche")
console.log(sportsCar.getName())) // Porche, 이때 getName 안의 this는 sportsCar를 가리키고
// 그안에는 this.name이 Porche로 초기화 되어있다.
Car.prototype.name = "Sonata" // 이건 새로 객체를 만든 것이 아니라 Car 생성자 함수 자체의 변수에 저장된다.
console.log(Car.prototype.getName()) // Sonata
같은 함수를 참조하지만 그 안에서 this는 각기 다른 객체를 가리킨다(그래서 getName 호출의 결과가 다르다). 결국 함수가 어떻게 호출되느냐에 따라 다르다는 것을 보여준다.
3. 생성자 함수 호출
function Car(name) {
this.name = name;
}
자바스크립트의 생성자 함수이다. 일반함수와 다른 점은 함수명이 대문자로 시작한다는 것뿐인데, new 연산자와 함께 호출이 되면 생성자 함수로 작동한다. 다만 new 가 없으면 다른 방식으로 호출되기 때문에 꼭 new 가 필요하다. 그리고 특정하게 강제되는 것이 없기 때문에 굳이 대문자로 시작하지 않아도 되지만 일반적으로 혼용을 방지하기 위해 함수명의 첫글자는 대문자로 쓴다.
function Car(name) {
this.name = name;
}
var sportsCar = new Car('Porche');
console.log(sportsCar); // Car {name: 'Porche'}
// new 연산자와 함께 생성자 함수를 호출하지 않으면 생성자 함수로 동작하지 않는다.
var someCar = Car('Sonata');
console.log(someCar); // undefined
생성자 함수에 대해 알아야 할 점은 new 연산자와 함께 호출되면 생성자 함수로 동작한다는 점과 생성자 함수로 동작하게 되면 리턴 문이 없어도 암묵적으로 생성자 함수로 만들어진 객체가 반환 된다는 점이다. (리턴문으로 다른 것을 반환하게 되면 생성자함수로 동작하지 않는다. 따라서 보통 리턴을 직접 적어주지 않는다.) 그리고 생성자 함수 안의 this 는 생성 된 객체를 가리키게 된다. 즉 sportsCar 객체를 가리킨다는 말이다. 더 자세한 내용은 new operator 관련 문서를 살펴보자.
생성자 함수 호출 시에 new 를 붙이지 않고 호출하면?
function Car(name) {
// 일반 함수 호출이라 생각하면 전역 객체에다가 name 프로퍼티를 추가하는 셈이된다.
this.name = name;
}
// new 연산자와 함께 생성자 함수를 호출하지 않으면 생성자 함수로 동작하지 않는다.
var someCar = Car('Sonata');
console.log(someCar); // undefined
console.log(this.name) // Sonata
일반 함수처럼 동작한다. 함수 내부 this는 전역을 가리키게 되고 someCar에도 아무것도 반환되지 않는다.
근데 자바스크립트 배열 생성자함수인 Array를 생각해보자
new Array(5) // [empty * 5]
Array(5) // [empty * 5]
얘기한 것과 다르다 왜일까... Scope-Safe Constructor 패턴이 사용되었기 때문이다. 이는 자바스크립트에 존재하는 생성자에서 적용되는데, new 를 썼으면 정상적으로 객체를 만들어서 반환하고 안썼으면 안쓴대로 또 처리해서 객체를 만들어 반환한다.
function A(arg) {
// Scope-Safe Constructor Pattern
// 생성자 함수가 new 연산자와 함께 호출되면 함수의 선두에서 빈객체를 생성하고 this에 바인딩한다.
// 즉 this가 이제 변수 a를 가리키게 된다.
// this가 호출된 함수(arguments.callee, 본 예제의 경우 A)의 인스턴스가 아니면 new 연산자를 사용하지 않은 것이므로
// 이 경우 new와 함께 생성자 함수를 호출하여 인스턴스를 반환한다.
// arguments.callee는 호출된 함수의 이름(A)을 나타낸다. 이 예제의 경우 A로 표기하여도 문제없이 동작하지만 (this instanceof A)
// 특정함수의 이름과 의존성을 없애기 위해서 arguments.callee를 사용하는 것이 좋다.
if (!(this instanceof arguments.callee)) {
return new arguments.callee(arg); // new A(arg)와 같다.
}
this.value = arg ? arg : 0;
}
var a = new A(100);
var b = A(10);
console.log(a.value); // 100
console.log(b.value); // 10
callee 는 현재 호출된 함수를 말한다. 위에서는 생성자 함수 A를 말한다.
4. call, apply, bind 와 함께 호출
Function.prototype.call, Function.prototype.apply 을 사용하면 함수를 호출할 때 명시적으로 어떤 객체에 this를 바인딩해줄 수 있다.
func.apply(thisArg, [argsArray])
func.call(thisArg, args...])
thisArg는 func 함수안의 this를 바인딩할 객체를 넣어준다. apply의 경우에는 두번째 파라미터에 인자로 넣을 배열을 전달하고, call의 경우에는 전달할 인자들을 나열한다. 여기서 알아야할 것은 apply, call은 함수 호출의 기능을 한다는 것이다.
이 메서드들이 주로 쓰이는 경우는 유사배열 객체인 arguments를 배열로 변환할 때이다
function func() {
var arr = Array.prototype.slice.apply(arguments)
arr.pop(); // [1, 2]
}
func(1,2,3);
유사배열객체인 arguments에는 array method를 쓸 수 없지만, apply와 call이 함수 호출을 가능하게 한다.
결국 위의 작업은 slice 메서드를 호출하고, this는 arguments 객체에 바인딩한다고 생각하면 된다.
function Car(name) {
this.name = name;
}
Car.prototype.go = function(callback) {
if(typeof callback == 'function') {
callback();
}
};
function foo() {
console.log(this.name); //
}
var car = new Car('Porche');
car.go(foo); // undefined
위 예제에서 foo는 콜백함수로 넘겨진다. 앞에서 말했듯이 콜백으로 넘겨지는 함수도 전역 객체를 가리킨다. 근데 go 메서드 안에서 쓰이는 this는 생성자함수로 만들어진 객체 car를 가리킨다. 따라서 메서드의 this를 가리킬 수 있도록하는 조치가 필요하다.
callback.apply(this) 혹은 callback.call(this)와 같이 callback 즉 foo 함수의 this를 메서드 안의 this로 바인딩하여 호출 해주면
"Porche" 라는 값을 출력한다.
그리고 bind를 활용한 호출도 가능하다.
function Car(name) {
this.name = name;
}
Car.prototype.go = function(callback) {
if(typeof callback == 'function') {
callback.bind(this)("Hi ");
}
};
function foo(greeting) {
console.log(greeting + this.name);
}
var car = new Car('Porche');
car.go(foo); // Hi Porche
bind 는 단순히 this를 바인딩해주는 역할만 하기 때문에 따로 호출해주어야한다. 호출하는 김에 파라미터도 넘겨보았다.
여기까지 this를 알아보았고, this는 결국 런타임에 어떻게 함수가 호출되느냐에 따라서 동적으로 바인딩이 된다.
이와 관련해서 Lexical scope가 선언된 위치를 기준으로 스코프가 정해지는 것(write-time)이라면 이와 반대로 dynamic scope는 함수의 선언 위치와 상관없이 호출되는 위치(runtime)에 따라서 스코프가 정해지는 것을 말한다.
그럼 this는 dynamic scope라고 말할 수 있는건가? 사실 javascript를 비롯한 많은 언어들이 lexical scope 규칙을 갖고 있다.
그래서 this는 dynamic scope 다! 라고 확실하게 말할 순 없더라도 함수가 호출되는 경우에 따라 this가 바인딩되는 곳이 다르기 때문에 그 비슷하다 라고 말할 수 있을 것 같다.
참조
https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Operators/new
'programming > javascript' 카테고리의 다른 글
Rest syntax VS Spread syntax (...) (0) | 2021.06.10 |
---|---|
자바스크립트 클로저(Closure) (0) | 2021.06.08 |
[webpack] 오래된 글을 보고 웹팩 설정을 하다가 바뀐 부분이 많아 고생했던 일 (0) | 2018.11.18 |
[jquery] offset (0) | 2018.03.28 |
[ES6] 클래스(class) (0) | 2018.02.06 |