대부분의 객체지향 언어에서 this는 클래스로 생성한 인스턴스 객체를 의미한다.
this는 작성시점이 아닌 런타임 시점에서 바인딩되며, 함수 호출 당시의 상황에 따라 컨텍스트가 결정된다.
자바스크립트에서 this는 기본적으로 실행 컨텍스트가 생성될 때(함수가 호출될 때), 실행 컨텍스트의 This Binding Component에 바인딩된다.
실행컨텍스트는 함수를 호출할 때(함수명() 으로 호출할 때) 생성되므로, this는 함수를 호출할 때 동적으로 결정된다.
함수가 선언될 때 함수의 상위 스코프를 결정하는 방식인 Lexical Scope와 헷갈리지 말자!
this 바인딩의 5가지 패턴
1. Global: window
2. Function 호출 : window
3. Method 호출 : object
4. constructor(new 키워드로 생성된 function영역의 this) : 새로 생성된 객체.
5. .call, .apply 호출 : call, apply의 첫 번째 인사로 명시된 객체
자바스크립트에서 this는 기본적으로 실행 컨텍스트가 생성될 때 함께 결정된다. 실행 컨텍스트 내 This Binding Component에 바인딩된다. 함수를 어떤 방식으로 호출하는가에 따라 값이 달라진다.
전역 공간에서 this는 전역 객체(Global Object)를 가리킨다.
window는 JavaScript에서 만든 것이 아니로, 전역 객체의 스코프도 아니다. window와 전역 객체를 같은 선상에서 사용한다. 개념상 전역 컨텍스트를 생성하는 주체(Host)가 전역 객체이기 때문이다.
window 객체와 같이 다른 객체를 마치 내 것처럼 사용하는 것을 Host 오브젝트 개념이라고 한다.
DOM 오브젝트도 Host 오브젝트이다.
참고로 브라우저 환경에서 전역객체는 window이고, Node.js 환경에서는 global이다. 환경에 따라 전역 객체를 가리키는 다양한 식별자가 존재한다. ES11 이상부터는 전역 객체의 이름을 globalThis
로 표준화하였다.
전역 객체 (global object)
코드가 실행되기 이전 단계에서 자바스크립트 엔진에 의해 어떤 객체보다도 먼저 생성되는 특수한 객체이며, 어떤 객체에도 속하지 않은 최상위 객체이다.
개발자가 의도적으로 생성할 수 없으며, 전역 객체를 생성할 수 있는 생성자 함수를 제공하지 않는다.
전역 객체는 표준 빌트인 객체(Object, String, Number, Function, Array 등)와 환경에 따른 호스트 객체(Web API)와 var 키워드로 선언한 전역 변수와 전역 함수를 프로퍼티로 갖는다.
이처럼 전역 변수를 선언하면 자바스크립트 엔진은 이를 전역객체의 프로퍼티로 할당한다. 즉, 전역 변수는 변수이면서 전역 객체의 프로퍼티이다.
아래의 코드는 browser에서의 예시이다. (globalThis가 window인 경우)
console.log(this === window); // true
이를 통해 this가 window를 참조하고 있음을 알 수 있다.
// var 키워드로 선언한 전역 변수
var a = 'test';
console.log(a); // test, 전역 변수
console.log(window.a); // test, window로 전역 변수 사용
console.log(this.a); // test, this로 전역 변수 사용
// this 키워드로 선언한 전역 변수
this.b = 'test';
console.log(window.b); // test
// 선언하지 않은 변수에 값을 할당한 암묵적인 전역방식. 전역 변수가 아니라 전역 객체의 프로퍼티이다.
c = 'test';
console.log(window.c); // test
window와 this로 전역 변수 사용한 예시이다.
this.b에 test값을 할당하였는데 window.b가 test가 나온 이유는 this는 window를 가리키기 때문이다.
const a = 'test;
console.log(a); // test
console.log(window.a); // undefined
console.log(this.a); // undefined
전역 변수와 전역 객체의 예시이다. let, const 키워드로 할당된 변수는 전역 객체에 프로퍼티로 할당하지 않기 때문에 마지막 예시에서의 window.a와 this.a의 값은 undefined가 나온다.
window.onload = function() {
console.log(this === window); // true
}
이를 통해 this가 window 객체를 참조하고 있음을 알 수 있다. onload 메소드가 실행되면(이벤트가 발생하면) 함수 실행 컨텍스트가 생성되고 ThisBinding 컴포넌트에 window가 할당된다. 그러므로 해당 코드는 true가 나온다.
window.onload = function() {
var a = 'test';
console.log(this.a); // undefined
}
a는 함수 내 지역변수이다. onload 메소드는 메소드이므로 this가 window를 가리키므로, this.a는 window.a이므로 'test'값을 지닌 a가 아니라 전역에 선언되어있는 a를 찾으려고 한다. 존재하지 않는 값이므로 undefined값이 나온다.
window.onload = function() {
this.a = 'test';
console.log(window.a); // test
}
onload 메소드의 this는 window 객체를 참조하므로 window 객체에 a가 선언 'test'값이 할당되어, 해당 결과는 'test'가 나온다.
함수명(프로퍼티) 앞에 객체가 명시되어있는 경우는 메소드로 호출한 것이다.
this에는 호출한 주체에 대한 정보가 담긴다. 즉, 메소드로서 호출하는 경우는 this가 함수명(프로퍼티명) 앞의 객체이다.
var obj = {
a: 1,
b: {
a: 2,
get: function() {
console.log(this === obj.b); // true
console.log(this.a); // 2
}
}
}
obj.b.get();
메소드로서 obj.b.get()를 호출하였다. 그러므로 obj.b.get()의 this는 obj.b이다.
this.a는 obj.b.a이므로 값은 2이다.
함수 vs. 메소드
함수는 그 자체로 독립적인 기능을 수행하지만, 메소드는 자신을 호출한 대상 객체에 관한 동작을 수행한다. 이처럼 독립성에 차이가 존재한다.
함수 앞에 점이나 대괄호가 없는 경우 함수로서 호출한 것이고, 있는 경우 메소드로서 호출한 것이다.
this에 바인딩될 값은 함수 호출 방식, 즉 함수가 어떻게 호출되었는지에 따라 동적으로 결정된다.
var obj = {
a: 1,
get: function() {
var a = 2;
console.log(this === window); // true
console.log(this.a); // undefined
}
}
var fn = obj.get();
fn();
fn은 함수로서의 호출이기 때문에 this는 전역 객체 즉, window를 바라본다.
window === window 이므로 첫번째 결과는 true가 나온다.
window.a는 존재하지 않으므로 undefined가 나온다.
ES6부터 나온 화살표 함수는 일반 함수에서 this가 GlobalThis를 바라보는 문제를 보완하기 위해 나왔다. 화살표 함수는 실행 컨텍스트 생성 시 arguments, super, this, new.target에 대한 로컬 바인딩하는 과정이 정의되지 않는다. arguments, super, this, new.target에 대한 참조는 lexical하게 둘러쌓여있는 환경의 바인딩으로 해석해야한다. 즉, 화살표 함수 내 this가 아예 없으며, 접근하려면 스코프 체인 상 가장 가까운 상위 스코프의 this를 그대로 접근한다는 뜻이다.
var obj = {
outer: function() {
console.log(this); // obj
var inner = () => {
console.log(this); // obj
}
inner();
}
}
obj.outer();
메소드로서의 호출한 obj.outer()의 this는 obj이다.
그러므로 결과 값은
{outer: ƒ}
outer: ƒ ()
[[Prototype]]: Object
로 나오게 된다. inner()는 일반 함수가 아닌 화살표 함수를 호출하였으므로, 상위 스코프의 this인 obj를 가리키게 되어 동일한 값이 출력된다.
객체 지향언어에서는 생성자는 클래스라고 하고 클래스를 통해 만든 객체를 인스턴스라고 한다.
인스턴스는 인스턴스마다의 고유 값을 가지는 것이 목적이며, 생성자는 구체적인 인스턴스를 만들기 위한 틀이라고 보면 된다.
new 키워드를 통해 함수를 호출하면 해당 함수가 생성자로서 동작하게 된다. 생성자 함수로서 호출된 경우 내부에서의 this는 새로 만들어지는 구체적인 인스턴스 자신이 된다.
생성자 함수를 호출하면
var Cat = function(name, age) {
this.bark = '야옹';
this.name = name;
this.age = age;
}
var choco = new Cat('초코', 7);
var navi = new Cat('나비', 5);
console.log(choco);
console.log(navi);
결과는
Cat {bark: "야옹", name: "초코", age: 7}
age: 7
bark: "야옹"
name: "초코"
[[Prototype]]: Object
Cat {bark: "야옹", name: "나비", age: 5}
age: 5
bark: "야옹"
name: "나비"
[[Prototype]]: Object
가 나오게 된다.
상황별로 this에 어떤 값이 바인딩되는 것 보다 별도의 대상에 명시적으로 this를 바인딩 하는 방법도 존재한다.
call 메소드는 메소드의 호출 주체인 함수를 즉시 실행하도록 하는 명령이다.
Function.prototype.call(thisArg[, arg1[, arg2[, ...]]]);
위 형태로 메소드의 첫 번째 인자를 this로 바인딩하고, 그 이후의 인자들은 호출할 함수의 매개변수가 된다. 파라미터 수가 고정적일 때 사용한다.
var obj = {
a: 1,
b: {
a: 2,
get: function() {
console.log(this.a);
}
}
}
obj.b.get.call(obj); // 1
obj.b.get.call(obj.b); // 2
첫 번째 메소드 호출은 this가 obj를 가리키며, get() 내부의 this는 obj가 된다. 그러므로 obj.a의 값은 1이므로 1이 출력된다.
두 번째 메소드 호출은 this가 obj.b를 참조하며, get() 내부의 this는 obj.b이다. obj.b.a의 값은 2이므로 2가 출력된다.
Function.prototype.apply(thisArg[, argsArray]);
위 형태로 메소드의 첫 번째 인자를 this로 바인딩하고, 그 이후의 단일 배열을 받아 배열의 요소들을 호출할 함수의 매개변수로 지정한다. 그러므로 파라미터 수가 유동적일 때 사용한다.
배열로 받지만 내부 요소들이 하나씩 매개변수로 매핑된다는게 신기하다.
var obj = {
a: 1,
get: function() {
console.log(this.a, Array.from(arguments));
}
}
obj.get.apply({ a: 4, b: 5 }, [7, 8]);
this가 { a: 4, b: 5 }이며, this.a는 4가 나온다.
두 번째 인자로 [7, 8]을 넘겨주어 파라미터 수가 유동적일 때 사용된다.
Function.prototype.bind(thisArg, [, arg1[, arg2[, ...]]])
ES5에 나온 문법인 bind는 call과 비슷하지만 즉시 호출하지 않고 넘겨받는 this와 전달인자들을 바탕으로 새로운 함수를 반환하기만 하는 메소드이다.
함수에 this를 미리 적용한다거나 함수에 넘길 파라미터를 일부 지정하는 부분 적용 함수를 구현하는 목적이 있다.