자바스크립트에서 this
는 기본적으로 실행 컨텍스트가 생성될 때 함께 결정된다. 실행 컨텍스트는 함수를 호출할 때 생성되므로, 바꿔 말하면 this
는 함수를 호출할 때 결정된다고 할 수 있다. 함수를 어떤 방식으로 호출하느냐에 따라 값이 달라진다.
전역 공간에서 this
는 전역 객체를 가리킨다. 개념상 전역 컨텍스트를 생성하는 주체가 바로 전역 객체이기 때문이다.
전역 객체는 자바스크립트 런타임 환경에 따라 다른 이름과 정보를 가지고 있다. 브라우저 환경에서 전역객체는 window
이고, Node.js 환경에서는 global
이다.
전역변수를 선언하면 자바스크립트 엔진은 이를 전역객체의 프로퍼티로도 할당한다. 변수이면서 객체의 프로퍼티이기도 한 셈이다.
var a = 1;
console.log(a); // 1
console.log(window.a); // 1
console.log(this.a); // 1
⇒ 전역공간에서 선언한 변수 a에 1을 할당했을 뿐인데 window.a와 this.a는 모두 1이 출력된다. 전역 공간에서의 this
는 전역객체를 의미하므로 두 값이 같은 값을 출력하는 것은 당연하지만, 그 값이 1인 것이 의아하다. 그 이유는 자바스크립트의 모든 변수는 실은 특정 객체의 프로퍼트로 동작하기 때문에다.
전역변수를 선언하면 자바스크립트 엔진은 이를 전역객체의 프로퍼티로 할당한다.
어떤 함수를 실행하는 방법은 여러가지가 있는데, 가장 일반적인 방법 두 가지는 함수로서 호출하는 경우와 메서드로서 호출하는 경우이다.
프로그래밍 언어에서 함수와 메서드는 미리 정의한 동작을 수행하는 코드 뭉치로, 이 둘을 구분하는 유일한 차이는 독립성에 있다.
함수는 그 자체로 독립적인 기능을 수행하는 반면, 메서드는 자신을 호출한 대상 객체에 관한 동작을 수행한다. 자바스크립트는 상황별로 this
키워드에 다른 값을 부여하게 함으로써 이를 구현했다.
어떤 함수를 객체의 프로퍼티에 할당한다고 해서 그 자체로서 무조건 메서드가 되는 것이 아니라 객체의 메서드로서 호출한 경우에만 메서드로 동작하고 그렇지 않으면 함수로 동작한다.
var func = function(x){
console.log(this, x);
};
func(1); // Window{ ... } 1
var obj = {
method: func
};
obj.method(2); // { method:f } 2
위의 예제에서 보듯이 '함수로서의 호출'과 '메서드로서의 호출'을 할때 this
가 가리키는 것은 다른다. '함수로서의 호출'과 '메서드로서의 호출'을 어떻게 구분할까? 함수 앞에 점(.
)이 있는지 여부만으로 간단하게 구분할 수 있다.
// 메서드로서의 호출 - 점 표기법, 대괄호 표기법
var obj = {
method : function(x) { console.log(this,x) }
};
obj.method(1); // { method: f } 1
obj['method'](2) // { method: f } 2
다시 말해 점 표기법이든 대괄호 표기법이든, 어떤 함수를 호출할 때 그 함수 이름(프로퍼티명) 앞에 객체가 명시돼 있는 경우에는 메서드로 호출한 것이고, 그렇지 않은 모든 경우에는 함수로 호출한 것이다.
this
에는 호출한 주체에 대한 정보가 담긴다. 어떤 함수를 메서드로서 호출하는 경우 호출 주체는 바로 함수명(프로퍼티명) 앞의 객체이다. 점 표기법의 경우 마지막 점 앞에 명시된 객체가 곧 this
가 되는 것이다.
var obj = {
methodA: function(){ console.log(this) },
inner : {
methodB : function(){ console.log(this) }
}
};
obj.methodA(); // { methodA : f, inner: {...} } ( === obj )
obj['methodA']; // { methodA : f, inner: {...} } ( === obj )
obj.inner.methodB(); // { method: f } ( === obj.inner)
obj.inner['methodB'](); // { method: f } ( === obj.inner)
obj['inner'].methodB(); // { method: f } ( === obj.inner)
obj['inner'].['methodB'](); // { method: f } ( === obj.inner)
어떤 함수를 함수로서 호출할 경우에는 this
가 지정되지 않는다. this
에는 호출한 주체에 대한 정보가 담긴다. 그런데 함수로서 호출하는 것은 호출 주체(객체지향언어에서의 객체)를 명시하지 않고 개발자가 코드에 직접 관여해서 실행한 것이기 때문에 호출 주체의 정보를 알 수 없는 것이다. 2장에서 실행 컨텍스트를 활성화할 당시에 this
가 지정되지 않은 경우 this
는 전역 객체를 바라본다고 했다. 따라서 함수에서의 this
는 전역 객체를 가리킨다.
더글라스 크락포드는 이를 명백한 설계상의 오류라고 지적한다. 그 이유를 알아보자.
var obj1 = {
outer: function(){
console.log(this); // (1):obj1
var innerFunc = function(){
console.log(this); // (2):전역객체(window) (3):obj2
}
innerFunc(); // 🌈
var obj2 = {
innerMethod : innerFunc
};
obj2.innerMethod(); // 🌈
}
}
obj1.outer();
this
를 바인딩한다. 이 함수는 호출할 때 함수명인 outer앞에 점(.
)이 있었으므로 메서드로서 호출한 것이다. 따라서 this
에는 마지막 점 앞의 객체인 obj1이 바인딩된다.this
바인딩 등을 수행한다. 이 함수를 호출할 때 함수명 앞에는 점(.
)이 없다. 즉 함수로서 호출한 것이므로 this
가 지정되지 않았고, 따라서 자동으로 스코프 체인상의 최상위 객체인 전역객체 (Window
)가 바인딩 된다.window
객체 정보가 출력된다.⇒ 7번째 줄에서는 outer 메서드 내부에 있는 함수(innerFunc)를 함수로서 호출했다. 반면 12번째 줄에서는 같은 함수(innerFunc)를 메서드로서 호출했다. 같은 함수임에도 7번째 줄에 의해 바인딩되는 this
와 12번째 줄에 의해 바인딩 되는 this
의 대상이 서로 달라진 것이다.
그러니까 this
바인딩에 관해서는 함수를 실행하는 당시의 주변 환경(메서드 내부인지, 함수 내부인지 등)은 중요하지 않고, 오직 해당 함수를 호출하는 구문 앞에 점 또는 대괄호 표기가 있는지 없는지가 관건인 것!!!
이렇게 하면 this
에 대한 구분은 명확히 할 수 있지만, 그 결과 this
라는 단어가 주는 인상과는 사뭇 달라져 버렸다. 호출 주체가 없을 때는 자동으로 전역객체를 바인딩하지 않고 호출 당시 주변환경의 this
를 그대로 상속받아 사용할 수 있다면 좋겠다. 그게 훨씬 자연스러울뿐더러 자바스크립트 설계상 이렇게 동작하는 편이 스코프 체인과의 일관성을 지키는 설득력 있는 방식이었다. 변수를 검색하면 우선 가장 가까운 스코프의 L.E를 찾고 없으면 상위 스코프를 탐색하듯이, this
역시 현재 컨텍스트에 바인딩된 대상이 없으면 직전 컨텍스트의 this
를 바라보도록 말이다.
아쉽게도 ES5까지는 자체적으로 내부함수에 this
를 상속할 방법이 없지만 다행히 이를 우회할 방법이 없지는 않다. 그 중 대표적인 방법은 바로 변수를 활용하는 것이다.
var obj1 = {
outer: function(){
console.log(this)
var innerFunc1 = function(){
console.log(this)
}
innerFunc1();
var self = this
var innerFunc2 = function(){
console.log(self)
}
innerFunc2()
}
}
obj1.outer();
위 예제의 innerFunc1 내부에서 this
는 전역객체를 가리킨다. 한편 outer 스코프에서 self라는 변수에 this
를 저장한 상태에서 호출한 innerFunc2의 경우 self에는 객체 obj가 출력된다.
ES6에서는 함수 내부에서 this
가 전역객체를 바라보는 문제를 보완하고자, this
를 바인딩하지 않는 화살표 함수를 새로 도입했다. 화살표 함수는 실행 컨텍스트를 생성할 때 this
바인딩 과정 자체가 빠지게 되어, 상위 스코프의 this
를 그대로 활용할 수 있다. 내부함수를 화살표 함수로 바꾸면 위의 예제의 '우회법'이 불필요해진다. (아쉽게도 ES5환경에서는 화살표 함수를 사용할 수 없다.)
var obj = {
outer: function(){
console.log(this) // (1) { outer: f }
var innerFunc = () => {
console.log(this) // (2) { outer: f }
}
innerFunc()
}
}
obj.outer();
그 밖에도 call, apply 등의 메서드를 활용해 함수를 호출할 때 명시적으로 this
를 지정하는 방법이 있다.
함수 A의 제어권을 다른 함수(또는 메서드) B에 넘겨주는 경우 함수 A를 콜백 함수라 한다. 이때 함수 A는 함수 B의 내부 로직에 따라 실행되며, this
역시 함수 B 내부 로직에서 정한 규칙에 따라 값이 결정된다. 콜백 함수도 함수이기 때문에 기본적으로 this
가 전역객체를 참조하지만, 제어권을 받은 함수에서 콜백 함수에 별도로 this
가 될 대상을 지정한 경우에는 그 대상을 참조하게 된다.
setTimeout(function() { console.log(this); }, 300); // --------- (1)
[1, 2, 3, 4, 5].forEach(function(x) { // --------- (2)
console.log(this,x);
});
document.body.innerHTML += '<button id="a">클릭</button>';
document.body.querySelector('#a')
.addEventListener('click', function(e){ // --------- (3)
console.log(this, e);
});
(1): setTimeout
함수는 300ms 만큼 시간 지연을 한 뒤 콜백 함수를 실행하라는 명령이다. 0.3초 뒤 전역객체가 출력된다.
(2): forEach
메서드는 배열의 각 요소를 앞에서부터 차레로 하나씩 꺼내어 그 값을 콜백 함수의 첫 번째 인자로 삼아 함수를 실행하라는 명령이다. 전역객체와 배열의 각 요소가 총 5회 출력된다.
(3): addEventListener
는 지정한 HTML 엘리먼트에 'click' 이벤트가 발생할 때마다 그 이벤트 정보를 콜백 함수의 첫 번째 인자로 삼아 함수를 실행하라는 명령이다. 버튼을 클릭하면 앞서 지정한 엘리먼트와 클릭 이벤트에 관한 정보가 담긴 객체가 출력된다.
⇒ (1)의 setTimeout
함수와 (2)의 forEach
메서드는 그 내부에서 콜백 함수를 호출할 때 대상이 될 this를 지정하지않는다. 따라서 콜백 함수 내부에서의 this는 전역객체를 참조한다. 한편 (3)의 addEventListener
메서드는 콜백 함수를 호출할 때 자신의 this를 상속하도록 정의돼 있다. 그러니까 메서드명의 점(.) 앞부분이 곧 this가 된다.
⇒ 이처럼 콜백 함수에서는 콜백 함수의 제어권을 가지는 함수(메서드)가 콜백 함수에서의 this를 무엇으로 할지 결정하며, 특별히 정의하지 않은 경우에는 기본적으로 함수와 마찬가지로 전역객체를 바라본다.
자바스크립트의 핵심 ! 호출한 것(객체) === this !!
생성자 함수는 어떤 공통된 성질을 지니는 객체들을 생성하는 데 사용하는 함수이다. 객체지향 언어에서는 생성자를 클래스, 클래스를 통해 만든 객체를 인스턴스라고 한다. 프로그래밍적으로 '생성자'는 구체적인 인스턴스를 만들기 위한 일종의 틀이다. 자바스크립트늩 함수에 생성자로서의 역할을 함께 부여했다. new 명령어와 함께 함수를 호출하면 해당 함수가 생성자로서 동작하게된다. 그리고 어떤 함수가 생성자 함수로서 호출된 경우 내부에서의 this는 곧 새로 만들 구체적인 인스턴스 자신이 된다.
⇒ 생성자 함수를 호출하면 우선 생성자의 prototype
프로퍼티를 참조하는 __proto__
라는 프로퍼티가 있는 객체를 만들고, 미리 준비된 공통 속성 및 개성을 해당 객체(this
)에 부여한다. 이렇게 해서 구체적인 인스턴스가 만들어진다.
var Cat = function (name, age) {
this.bark = '야옹'
this.name = name
this.age = age
}
var choco = new Cat ('초코', 7) // --- (1)
var nabi = new Cat ('나비', 5) // --- (2)
console.log(choco, nabi)
/* 결과
Cat { bark: '야옹', name: '초코', age: 7 }
Cat { bark: '야옹', name: '나비', age: 5 }
*/
Cat이란 변수에 익명 함수를 할당했다. 이 함수 내부에서의 this
에 접근해서 bark, name, age 프로퍼티에 각각 값을 대입한다. 그리고 new 명령어와 함께 Cat 함수를 호출해서 변수 choco, nabi에 각각 할당했다. choco와 nabi를 출력해보니 각각 Cat 클래스의 인스턴스 객체가 출력된다. 즉 (1)에서 실행한 생성자 함수 내부에서의 this
는 choco 인스턴스를, (2)에서 실행한 생성자 함수 내부에서의 this
는 nabi 인스턴스를 가리킴을 알 수 있다.
Function.prototype.call(thisArg[, arg1[, arg2[, ...]]])
call
메서드는 메서드의 호출 주체인 함수를 즉시 실행하도록 하는 명령이다. 이때 call
메서드의 첫 번째 인자를 this
로 바인딩하고, 이후의 인자들을 호출할 함수의 매개변수로 한다. 함수를 그냥 실행하면 this
는 전역객체를 참조하지만 call
메서드를 이요하면 임의의 객체를 this
로 지정할 수 있다.
var func = function (a, b, c) {
console.log(this, a, b, c)
}
func(1, 2, 3) // Window{...} 1 2 3
func.call({ x: 1 }, 4, 5, 6) // { x: 1 } 4 5 6
메서드에 대해서도 마찬가지로 객체의 메서드를 그냥 호출하면 this
는 객체를 참조하지만 call
메서드를 이용하면 임의의 객체를 this
로 지정할 수 있다.
var obj = {
a : 1,
method : function (x, y) {
console.log(this.a, x, y)
}
}
obj.method(2, 3) // 1 2 3
obj,method.call({ a: 4 }, 5, 6) // 4 5 6
Function.prototype.apply(thisArg[, argsArray])
apply
메서드는 call
메서드와 기능적으로 완전히 동일하다. call
메서드는 첫 번째 인자를 제외한 나머지 모든 인자들을 호출할 함수의 매개변수로 지정하는 반면, apply
메서드는 두 번째 인자를 배열로 받아 그 배열의 요소들을 호출할 함수의 매개변수로 지정한다는 점에서만 차이가 있다.
var func = function (a, b, c) {
console.log(this, a, b, c)
}
func.apply({ x: 1 }, [4, 5, 6]) // { x: 1 } 4 5 6
var obj = {
a: 1,
method: function(x, y) {
console.log(this.a, x, y)
}
}
obj.method.apply({ a: 4 },[5, 6]) // 4 5 6
Function.prototype.bind(thisArg[, arg1[, arg2[, ...]]])
bind
메서드는 ES5에 추가된 기능으로, call
과 비슷하지만 즉시 호출하지는 않고 넘겨받은 this
및 인수들을 바탕으로 새로운 함수를 반환하기만 하는 메서드이다. 다시 새로운 함수를 호출할 때 인수를 넘기면 그 인수들은 기존 bind
메서드를 호출할 때 전달했던 인수들의 뒤에 이어서 등록된다. 즉 bind
메서드는 함수에 this
를 미리 적용하는 것과 부분 적용 함수를 구현하는 두 가지 목적을 모두 지닌다.
var func = function (a, b, c, d) {
console.log(this, a, b, c, d)
}
func(1, 2, 3, 4) // Window{ ... } 1 2 3 4
var bindFunc1 = func.bind({ x: 1 })
bindFunc1(5, 6, 7, 8); // { x: 1 } 5 6 7 8
var bindFunc2 = func.bind({ x: 1 }, 4, 5)
bindFunc2(6, 7) // { x: 1 } 4 5 6 7
bindFunc2(8, 9) // { x: 1 } 4 5 8 9
this
를 { x: 1 }로 지정한 새로운 함수가 담긴다.this
를 { x: 1 }로 지정하고, 앞에서부터 두 개의 인수를 각각 4, 5로 지정한 새로운 함수를 담았다.this
값이 바뀐 것을 제외하고는 최초 func함수에 4, 5, 6, 7을 넘긴 것과 같은 동작을 한다. 11번째 줄에서도 마찬가지이다.bind
는 this
만을 지정한 것이고, 9번째 줄의 bind
는 this
지정과 함께 부분 적용 함수를 구현한 것이다.bind
메서드를 적용해서 새로 만든 함수는 한 가지 독특한 성질이 있다. 바로 name 프로퍼티에 동사 bind
의 수동태인 'bound'라는 접두어가 붙는다는 점이다. 어떤 함수의 name 프로퍼티가 'bound xxx'라면 이는 곧 함수명이 xxx인 원본 함수에 bind
메서드를 적용한 새로운 함수라는 의미가 되므로 기존의 call
이나 apply
보다 코드를 추적하기에 더 수월해진 면이 있다.
var func = function (a, b, c, d) {
console.log(this, a, b, c, d)
}
var bindFunc = func.bind({ x: 1 }, 4, 5)
console.log(func.name) // func
console.log(bindFunc.name) // bound func
위에서 메서드의 내부함수에서 메서드의 this
를 그대로 바라보게 하기 위한 방법으로 self 등의 변수를 활용한 우회법을 소개했는데, call
, apply
또는 bind
메서드를 이요하면 더 깔끔하게 처리할 수 있다.
// 내부함수에 this 전달 - call
var obj = {
outer: function(){
console.log(this)
var innerFunc = function(){
console.log(this)
}
innerFunc.call(this)
}
}
obj.outer();
// 내부함수에 this 전달 - bind
var obj = {
outer: function(){
console.log(this)
var innerFunc = function(){
console.log(this)
}.bind(this)
innerFunc()
}
}
obj.outer();
또한 콜백 함수를 인자로 받는 함수나 메서드 중에서 기본적으로 콜백 함수 내에서의 this
에 관여하는 함수 또는 메서드에 대해서도 bind
메서드를 이용하면 this
값을 사용자의 입맛에 맞게 바꿀 수 있다.
vat obj = {
logThis: function(){
console.log(this)
},
logThisLater1: function(){
setTimeout(this.logThis, 500)
},
logThisLater2: function(){
setTimeout(this.logThis.bind(this), 1000)
}
}
obj.logThisLater1() // Window { ... }
obj.logTHisLater2() // obj { logThis: f, ... }
ES6에 새롭게 도입된 화살표 함수는 실행 컨텍스트 생성 시 this
를 바인딩하는 과정이 제외되었다. 즉 이 함수 내부에는 this
가 아예 없으며, 접근하고자 하면 스코프체인상 가장 가까운 this
에 접근하게 된다.
var obj = {
outer: function(){
console.log(this)
var innerFunc = () => {
console.log(this)
}
innerFunc()
}
}
obj.outer()
이렇게 화살표 함수를 사용하면 별도의 변수로 this
를 우회하거나 call
/apply
/bind
를 적용할 필요가 없어 더욱 간결하고 편리하다.
콜백 함수를 인자로 받는 메서드 중 일부는 추가로 this
로 지정할 객체를 인자로 지정할 수 있는 경우가 있다. 이러한 메서드의 thisArg 값을 지정하면 콜백 함수 내부에서 this
값을 원하는 대로 변경할 수 있다. 이런 형태는 여러 내부 요소에 대해 같은 동작을 반복 수행해야 하는 배열 메서드에 많이 포진돼 있으며, 같은 이유로 ES6에 새로 등장한 Set, Map 등의 메서드에도 일부 존재한다. 그중 대표적인 배열 메서드인 forEach의 예를 살펴보겠다.
var report = {
sum: 0,
count: 0,
add: function(){
var args = Array.prototype.slice.call(arguments)
args.forEach(function(entry){
this.sum += entry
++this.count
}, this)
},
average: function(){
return this.sum / this.count
}
}
report.add(60, 85, 90)
console.log(report.sum, report.count, report.average()) // 240 3 80
this
는 add 메서드에서의 this
가 전달된 상태이므로 add 메서드의 this
(report)를 그대로 가리키고 있다. 따라서 배열의 세 요소를 순회하면서 report.sum 값 및 report.count 값이 차례로 바뀌고, 순회를 마친 결과 report.sum에는 240이 report.count에는 3이 담기게 된다.// 콜백 함수와 함께 thisArg를 인자로 받는 메서드
Array.prototype.forEach(callback[, thisArg])
Array.prototype.map(callback[, thisArg])
Array.prototype.filter(callback[, thisArg])
Array.prototype.some(callback[, thisArg])
Array.prototype.every(callback[, thisArg])
Array.prototype.find(callback[, thisArg])
다음 규칙은 명시적 this 바인딩이 없는 한 늘 성립한다.
this
는 전역객체(브라우저에서는 window, Node.js에서는 global)를 참조한다.this
는 메서드 호출 주체(메서드명 앞의 객체)를 참조한다.this
는 전역객체를 참조한다. 메서드의 내부함수에서도 같다.this
는 해당 콜백 함수의 제어권을 넘겨받은 함수가 정의한 바에 따르며, 정의하지 않은 경우에는 전역객체를 참조한다.this
는 생성될 인스턴스를 참조한다.다음은 명시적 this의 바인딩이다. 위 규칙에 부합하지 않는 경우에는 다음 내용을 바탕으로 this를 예측할 수 있다.
call
, apply
메서드는 this
를 명시적으로 지정하면서 함수 또는 메서드를 호출한다. bind
메서드는 this
및 함수에 넘길 인수를 일부 지정해서 새로운 함수를 만든다. this
를 받기도 한다.
정말 세세하게 잘 적어주셔서 너무 잘 보고 갑니다 :)