자바스크립트에서 this 는 어디서든 사용할 수 있습니다.
상황에 따라 this 가 바라보는 대상이 달라지는데, 어떤 이유로 그렇게 되는지 파악하기 힘든 경우도 있고 예상과 다르게 엉뚱한 대상을 바라보는 경우도 있습니다.
함수와 객체의 구분이 느슨한 자바스크립트에서 this 는 실질적으로 이 둘을 구분하는 거의 유일한 기능입니다.
상황별로 this 가 어떻게 달라지는지, 왜 그렇게 되는지, 예상과 다른 대상을 바라보고 있을 경우 그 원인을 효과적으로 추적하는 방법을을 알아보겠습니다.
자바스크립트에서 this는 기본적으로 실행 컨텍스트가 생성될 때 함께 결정됩니다. 실행 컨텍스트는 함수를 호출할 때 생성되므로, 바꿔 말하면 this는 함수를 호출할 때 결정된다고 볼 수 있습니다. 함수를 어떤 방식으로 호출하느냐에 따라 값이 달라지는 것입니다.
전역 공간에서 this는 전역 객체를 가리킵니다. 개념상 전역 컨텍스트를 생성하는 주체가 바로 전역 객체이기 때문입니다. 전역 객체는 자바스크립트 런타임 환경에 따라 다른 이름과 정보를 가지고 있습니다. 브라우저 환경에서 전역 객체는 window 이고 Node.js 환경에서는 global입니다.
전역 변수를 선언하면 자바스크립트 엔진은 이를 전역객체의 프로퍼티로 할당합니다.
// 브라우저
console.log(this); // window
// node.js
console.log(this); // global
프로그래밍 언어에서 함수와 메서드는 미리 정의한 동작을 수행하는 코드 뭉치로, 이 둘을 구분하는 유일한 차이는 독립성에 있습니다.
함수는 그 자체로 독립적인 기능을 수행하는 반면, 메서드는 자신을 호출한 대상 객체에 관한 동작을 수행합니다. 자바스크립트는 상황별로 this 키워드에 다른 값을 부여하게 함으로써 이를 구현했습니다
const func = function(x) {
console.log(this, x);
};
const obj = {
method: func
};
func(1); // Window { ... } 1 함수 호출
obj.method(2); // { method: f } 2 메서드 호출
obj 의 method 프로퍼티에 할당한 값과 func 변수에 할당한 값은 모두 1번째 줄에서 선언한 함수를 참조합니다. 이를 변수에 담아 호출한 경우와 obj 객체의 프로퍼티에 할당해서 호출한 경우 this 가 달라지는 것입니다.
함수 호출과 메서드 호출 어떻게 구분할까요?
함수 앞에 점( .)이 있는지 여부만으로 간단하게 구분할 수 있습니다.
func(1) 은 앞에 점이 없으므로 함수로서 호출한 것이고
obj.method 에서 method 앞에 점이 있으니 메서드로서 호출한 것입니다.
this에는 호출한 주체에 대한 정보가 담깁니다. 어떤 함수를 메서드로서 호출하는 경우 호출 주체는 바로 함수명 앞의 객체입니다.
const obj = {
methodA: function () {
console.log(this);
},
inner: {
methodB: function() {
console.log(this);
}
}
};
obj.methodA(); // { methodA: f, inner {...} }
obj.inner.methodB(); // { methodB: f }
어떤 함수를 함수로서 호출할 경우 this가 지정되지 않습니다. this에는 호출한 주체에 대한 정보가 담깁니다. 그런데 함수로서 호출하는 것은 호출 주체를 명시하지 않고 개발자가 코드에 직접 관여해서 실행한 것이기 때문에 호출 주체의 정보를 알 수 없는 것입니다. 실행 컨텍스트를 활성화 할 당시에 this가 지정되지 않을 경우 this는 전역 객체를 바라봅니다. 따라서 함수에서의 this는 전역 객체를 가리킵니다.
우리는 이미 어떤 함수를 메서드로서 호출할 때와 함수로서 호출할 때 this가 무엇을 가리키는지를 알고 있습니다. 내부함수 역시 이를 함수로서 호출했는지 메서드로서 호출했는지만 파악하면 this의 값을 정확히 맞출 수 있습니다.
const obj1 = {
outer: function () {
console.log(this); // (1)
const innerFunc = function () {
console.log(this); // (2) (3)
}
innerFunc();
const obj2 = {
innerMethod: innerFunc
};
obj2.innerMethod();
}
};
obj1.outer();
(1): obj1, (2): 전역객체(Window), (3):obj2입니다.
const obj = {
outer: function() {
console.log(this); // { outer: f }
const innerFunc1 = function() {
console.log(this); // Window { ... }
}
innerFunc1();
const self = this;
const innerFunc2 = function() {
console.log(self); // { outer: f }
}
innerFunc2();
}
}
obj.outer();
innerFunc1 내부에서 this는 전역객체를 가리킵니다. 한편 outer 스코프에서 self 라는 변수에 this 를 저장한 상태에서 호출한 innerFunc2 의 경우 self에는 객체 obj 가 출력됩니다.
ES6에서는 함수 내부에서 this가 전역객체를 바라보는 문제를 보완하고자, this를 바인딩 하지 않는 화살표 함수를 새로 도입했습니다. 화살표 함수는 실행 컨텍스트를 생성할 때 this 바인딩 과정 자체가 빠지게 되어, 상위 스코프의 this를 그대로 활용할 수 있습니다.
내부함수를 화살표 함수로 바꾸면 우회법이 불필요해집니다.
const obj = {
outer: function() {
console.log(this); // { outer: f }
const innerFunc = () => {
console.log(this); // { outer: f }
};
innerFunc();
}
};
obj.outer();
그 밖에도 call, apply 등의 메서드를 활용해 함수를 호출할 때 명시적으로 this를 지정하는 방법이 있습니다.
함수 A의 제어권을 다른 함수 B에게 넘겨주는 경우 함수 A를 콜백 함수라 합니다. 이때 함수 A는 함수 B의 내부 로직에 따라 실행되며, this 역시 함수 B 내부로직에서 정한 규칙에 따라 값이 결정됩니다.
콜백 함수도 함수이기 때문에 기본적으로 this가 전역객체를 참조하지만, 제어권을 받은 함수에서 콜백 함수에 별도로 this가 될 대상을 지정한 경우에는 그 대상을 참조하게 됩니다.
setTimeout(function() { console.log(this); }, 300);
[1, 2, 3, 4, 5].forEach(function(x) {
console.log(this, x);
});
document.body.innerHTML += '<button id="a">클릭</button>';
document.body.querySelector('#a')
.addEventListener('click', function (e) { console.log(this, e); });
setTimeout 함수와 forEach 메서드는 그 내부에서 콜백 함수를 호출할 때 대상이 될 this를 지정하지 않습니다. 따라서 콜백 함수 내부에서의 this는 전역객체를 참조합니다. 한편 addEventListener 메서드는 콜백 함수를 호출할 때 자신의 this를 상속하도록 정의돼 있습니다.
그러니까 메서드명의 점(.) 앞부분이 곧 this가 되는 것이죠.
콜백 함수의 제어권을 가지는 함수(메서드)가 콜백 함수에서의 this를 무엇으로 할지를 결정하며, 특별히 정의하지 않은 경우에는 기본적으로 함수와 마찬가지로 전역객체를 바라봅니다.
생성자 함수를 호출하면 우선 생성자의 prototype 프로퍼티를 참조하는 proto 라는 프로퍼티가 있는 객체(인스턴스)를 만들고, 미리 준비된 공통 속성 및 개성을 해당 객체(this)에 부여합니다. 이렇게 해서 구체적인 인스턴스가 만들어집니다.
const Cat = function(name, age) {
this.bark = '야옹'
this.name = name;
this.age = age;
}
const cho = new Cat('쵸', 7);
console.log(cho);
// Cat { bark : '야옹', name: '쵸', age: 7 }
Cat 이란 변수에 익명 함수를 할당했습니다. 이 함수는 내부에서 this에 접근해서 bark, name, age 프로퍼티에 각각 값을 대입합니다. new 명령어와 함께 Cat 함수를 호출해서 변수 cho 에 할당했습니다. 출력해보니 Cat 클래스의 인스턴스 객체가 출력됩니다. 즉 생성자 함수 내부에서의 this 는 cho 를 가리킴을 알 수 있습니다.
Function.prototype.call(thisArg[, arg1[, arg2[, ...]]])
call 메서드는 메서드의 호출 주체인 함수를 즉시 실행하도록 하는 명령입니다.
call 메서드의 첫 번째 인자를 this로 바인딩하고, 이후의 인자들을 호출할 함수의 매개변수로 합니다.
const 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로 지정할 수 있습니다.
Function.prototype.apply(thisArg, [, argsArray])
apply 메서드는 call 메서드와 기능적으로 완전히 동일합니다.
다만 apply 메서드는 두 번째 인자를 배열로 받아 그 배열의 요소들을 호출할 함수의 매개변수로 지정한다는 점에서만 차이가 있습니다.
생성자 내부에 다른 생성자와 공통된 내용이 있을 경우 call 또는 apply를 이용해 다른 생성자를 호출하면 간단하게 반복을 줄일 수 있습니다.
function Person(name, gender) {
this.name = name
this.gender = gender
}
function Student(name, gender, school) {
Person.call(this,name,gender);
this.school = school;
}
function Employee(name, gender, company) {
Person.apply(this,[name, gender]);
this.company = company;
}
const a = new Student ('a', 'female', '서울대');
const b = new Employee ('b', 'male', '네이버');
Function.prototype.bind(thisArg[, arg1[, arg2[, ...]]])
ES5에서 추가된 기능으로, call과 비슷하지만 즉시 호출하지는 않고 넘겨 받은 this 및 인수들을 바탕으로 새로운 함수를 반환하기만 하는 메서드입니다.
다시 새로운 함수를 호출할 때 인수를 넘기면 그 인수들은 기존 bind 메서드를 호출할 때 전달했던 인수들의 뒤에 이어서 등록됩니다. 즉 bind 메서드는 함수에 this를 미리 적용하는 것과 부분 적용 함수를 구현하는 두 가지 목적을 모두 지닙니다.
const func = function(a, b, c, d) {
console.log(this, a, b, c, d);
};
func(1, 2, 3, 4); // Window{ ... } 1 2 3 4
const bindFunc1 = func.bind({ x : 1 });
bindFunc1(5, 6, 7, 8); // { x: 1 } 5 6 7 8
const 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
ES6에 새롭게 도입된 화살표 함수는 실행 컨텍스트 생성 시 this를 바인딩하는 과정이 제외됐습니다. 즉 이 함수 내부에는 this가 아예 없으며, 접근하고자 하면 스코프체인상 가장 가까운 this에 접근하게 됩니다.
const obj = {
outer: function() {
console.log(this); // { outer: f }
const innerFunc = () => {
console.log(this); // { outer: f }
};
innerFunc();
}
}
obj.outer();
콜백 함수를 인자로 받는 메서드 중 일부는 추가로 this로 지정할 객체를 인자로 지정할 수 있는 경우가 있습니다. 이러한 메서드의 thisArg값을 지정 하면 콜백 함수 내부에서 this 값을 원하는 대로 변경할 수 있습니다.
이런 형태는 여러 내부 요소에 대해 같은 동작을 반복 수행해야하는 배열 메서드에 많이 포진돼 있으며, 같은 이유로 ES6에서 새로 등장한 Set, Map 등의 메서드에도 일부 존재합니다.
배열 메서드인 forEach의 예를 살펴보겠습니다.
const report = {
sum: 0,
count: 0,
add: function() {
const 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, 95);
console.log(report.sum, report.count, report.average()); // 240 3 80
report 객체에는 sum, count 프로퍼티가 있고, add, average 메서드가 있습니다.
add 메서드는 arguments를 배열로 변환해서 args 변수에 담고, 이 배열을 순회하면서 콜백 함수를 실행하는데, 이때 콜백 함수 내부에서의 this는 forEach 함수의 두 번째 인자로 전달해준 this가 바인딩 됩니다.
60, 85, 95를 인자로 삼아 add 메서드를 호출하면 이 세 인자를 배열로 만들어 forEach 메서드가 실행됩니다. 콜백 함수 내부에서의 this는 add 메서드에서의 this가 전달된 상태이므로 add 메서드의 this(report)를 그대로 가리키고 있습니다. 따라서 배열의 세 요소를 순회하면서 report.sum 값 및 report.count 값이 차례로 바뀌고, 순회를 마친 결과 report.sum에는 240이, report.count 에는 3이 담기게 됩니다.
다음 규칙은 명시적 this 바인딩이 없는 한 늘 성립합니다.
- 전역공간에서의 this는 전역객체를 참조합니다.
- 어떤 함수를 메서드로서 호출한 경우 this는 메서드 호출 주체를 참조합니다.
- 어떤 함수를 함수로서 호출한 경우 this는 전역객체를 참조합니다. 메서드 내부함수에서도 같습니다.
- 콜백 함수 내부에서의 this는 해당 콜백 함수의 제어권을 넘겨받은 함수가 정의한 바에 따르며, 정의하지 않은 경우에는 전역객체를 참조합니다.
- 생성자 함수에서의 this는 생성될 인스턴스를 참조합니다.
다음은 명시적 this 바인딩입니다.
- call, apply 메서드는 this를 명시적으로 지정하면서 함수 또는 메서드를 호출합니다.
- bind 메서드는 this 및 함수에 넘길 인수를 일부 지정해서 새로운 함수를 만듭니다.
- 요소를 순회하면서 콜백 함수를 반복 호출하는 내용의 일부 메서드는 별도의 인자로 this를 받기도 합니다.