자바스크립트에서의 this는 클래스 뿐만아니라 어디서든 사용할 수 있다. 상황에 따라 this가 바라보는 대상은 달라지기 때문에 정확한 작동 방식을 이해하지 못하면 원인을 파악해서 해결할 수 없다.
상황에 따라 달라지는 this
자바스크립트에서 this는 기본적으로 실행 컨텍스트가 생성될 떼 함께 결정된다. 실행 컨텍스트는 함수를 호출할 때 생성되므로 this는 함수를 호출할 때 결정된다고 할 수 있다.
전역 공간에서의 this
전역 공간에서 this는 전역 객체를 가리킨다. 전역 객체는 자바스크립트 런타임 환경에 따라 다른 이름과 정보를 가진다. 브라우저 환경에서 전역객체는 window, Node.js 환경에서는 global이다.
전역 공간에서만 발생하는 특이한 성질
전역변수를 설정하면 자바스크립트 엔진은 전역객체의 프로퍼티로 할당한다. 변수이면서 객체의 프로퍼티인 것이다.
var a = 1;
console.log(a) // 1
console.log(window.a) // 1
console.log(this.a) // 1
즉, 자바스크립트의 모든 변수는 실은 특정 객체의 프로퍼티로서 동작한다. 특정 객체란 바로 실행 컨텍스트의 LexicalEnvironment(L.E)이다. a를 직접 호출할 때도 변수 a에 접근하기 위해 스코프 체인에서 a를 검색하다가 가장 마지막에 도달하는 전역 스코프의 L.E, 즉 전역객체에서 해당 프로퍼티 a를 발견해서 그 값을 반환한 것이다.
따라서 전역 공간에서 var로 변수를 선언하는 것과 window의 프로퍼티에 직접 할당하는 것은 같은 동작을 한다. 그러나 delete 연산자를 쓰는 경우에는 다르게 작동한다. 전역객체의 프로퍼티로 할당한 경우에는 delete를 사용하여 삭제할 수 있으나 전역변수로 선언한 경우에는 삭제되지 않는다.
메서드로서 호출할 때 그 메서드 내부에서의 this
함수를 실행하는 방법은 함수를 호출하는 경우와 메서드를 호출하는 경우 두 가지가 있다. 이 둘을 구분하는 유일한 차이는 독립성이다. 함수는 그 자체로 독립적인 기능을 수행하는 반면, 메서드는 자신을 호출한 대상 객체에 관한 동작을 수행한다.
var func = function(x) {
console.log(this, x);
}
// 함수로서 호출
function(1); // Window {...} 1
var obj = {
method: func
};
// 메서드로서 호출
obj.method(2); // {method: f} 2
obj['method'](2) // {method: f} 2
이 둘을 구분하는 방법은 함수 앞에 점(.)이 있는지 여부만으로 간단하게 구분할 수 있다. 점(.) 앞에 객체가 명시되어 있는 경우에는 메서드로 호출한 것이다.(대괄호 표기법 또한 메서드로서의 호출)
메서드로 호출한 경우에, this에는 호출한 주체에 대한 정보가 담기는데 이는 호출되는 함수가 속한 객체이다.
함수로서 호출할 때 그 함수 내부에서의 this
어떤 함수를 함수로서 호출할 경우에는 this가 지정되지 않는다. 따라서 이 경우 this는 전역 객체를 가리킨다. 더글라스 크락포드는 이를 명백한 설계상의 오류라고 지적한다.
아래 코드는 설계상의 오류로 인해 나타나는 문제다.
var obj = {
outer: function() {
console.log(this); // (1)
var innerFunc = function() {
console.log(this); // (2)(3)
}
innerFunc();
var obj2 = {
innerMethod: innerFunc
};
obj2.innerMethod();
}
};
obj1.outer();
각 번호의 정답은 (1): obj1, (2): 전역객체(Window), (3): obj2 이다.
특히, (2)와 (3)의 innerFunc 함수는 호출 방식에 따라 this의 값이 달라졌다. (2)는 innerFunc함수를 함수로서 호출했고, (3)은 innerFunc함수를 메서드로서 호출했다.
즉, this 바인딩에 관해서는 함수를 실행하는 당시의 주변 환경(메서드 내부인지, 함수 내부인지 등)은 중요하지 않고, 오직 해당 함수를 호출하는 구문 앞에 점 또는 대괄효 표기가 있는지 없는지가 관건인 것이다.
이를 해결하는 방법은 변수를 활용하는 것이다. (ES5까지)
var obj = {
outer: function() {
console.log(this); // (1) {outer: f}
var innerFunc1 = function() {
console.log(this); // (2) Windonw{...}
}
innerFunc1();
var self = this;
var innerFunc2 = function() {
console.log(self); // (3) {outer: f}
}
innerFunc2();
}
};
obj1.outer();
innerFunc1 함수 내부에서 this는 여전히 전역객체를 가리키지만, outer 스코프에서 self라는 변수에 this를 저장하고 호출한 innerFunc2 함수는 객체 obj를 가리키게 된다.
상위 스코프의 this를 저장해서 활용하기 위한 변수명에는 self 외에도 _this, that, _ 등 여러 가지가 있다.
var obj = {
outer: function () {
console.log(this); // (1) {outer: f}
}
var innerFunc = () => {
console.log(this); // (2) {outer: f}
};
innerFunc();
};
obj.outer();
ES6에서는 이 문제를 보완하고자 this를 바인딩하지 않는 화살표 함수를 새로 도입했다. 화살표 함수는 실행 컨텍스트를 생성할 때 this 바인딩 과정 자체가 빠지게 되어, 상위 스코프의 this를 그대로 활용할 수 있다. 따라서 위와 같이 변수를 설정하는 '우회법'은 사용하지 않아도 된다.
obj1.method()는 일반 함수로 선언된 메서드를 호출한 경우다. 여기서 this는 호출하는 객체를 참조하므로, this는 obj1이 된다.
반면에, obj2.method()는 화살표 함수로 선언된 메서드를 호출했다. 화살표 함수는 this가 정의될 때의 상위 스코프에서 값을 lexical하게 가져온다. 즉, 화살표 함수는 호출되는 방식과 상관없이 함수가 정의된 위치에 따라 상위 함수 스코프 또는 전역 스코프에서 this를 가져온다. obj2.method()의 this는 화살표 함수가 정의된 위치인 전역 스코프의 this를 참조하게 되므로 브라우저 환경에서 Window 객체가 된다.
콜백 함수 호출 시 그 함수 내부에서의 this
함수 A의 제어권을 다른 함수(또는 메서드) B에게 넘겨주는 경우 함수 A를 콜백 함수라 한다. 콜백 함수도 함수이기 때문에 기본적으로 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를 지정하지 않으므로 전역 객체를 참조한다.
반면에, addEventListener 메서드는 콜백 함수를 호출할 때 자신의 this를 상속하도록 정의돼 있다. 따라서 버튼을 클릭하면 앞서 지정한 엘리먼트와 클릭 이벤트에 관한 정보가 담긴 객체가 출력된다.
생성자 함수 내부에서 this
생성자 함수는 어떤 공통된 성질을 지니는 객체들을 생성하는 데 사용하는 함수다. 프로그래밍적으로 '생성자'는 구체적인 인스턴스를 만들기 위한 일종의 틀이다. 자바스크립트는 함수에 생성자로서의 역할을 함께 부여돼 있다. new 명령어와 함께 함수를 호출하면 해당 함수가 생성자로서 동작한다. 그리고 어떤 함수가 생성자 함수로서 호출된 경우 내부에서의 this는 곧 새로 만들 구체적인 인스턴스 자신이 된다.
var Cat = function(name, age) {
this.bark = '야옹';
this.name = name;
this.age = age;
}
var choco = new Cat('초코', 7);
var nabi = new Cat('나비', 5);
console.log(choco, nabi);
/* 결과
Cat {bark: '야옹', name: '초코', age: 7}
Cat {bark: '야옹', name: '나비', age: 5}
*/
위 코드를 통해 생성자 함수 내부에서 this에는 각각 choco 인스턴스와 nabi 인스턴스를 가리키고 있음을 알 수 있다.
명시적으로 this를 바인딩하는 방법
this에 별도의 대상을 바인딩하는 방법도 있다.
call 메서드
Function.prototype.call(thisArg[, arg1[, arg2[, ...]]])
call 메서드는 메서드의 호출 주체인 함수를 즉시 실행하도록 하는 명령이다. 첫 번째 인자를 this로 바인딩하고, 이후 인자들을 호출할 함수의 매개변수로 한다. 함수를 그냥 실행하면 this는 전역객체를 참조하지만 call 메서드를 이용하면 임의의 객체를 this로 지정할 수 있다.
var func = function(a,b,c) {
console.log(this, a, b, c);
}
func(1, 2, 3); // Window{...}123
func.call({x: 1}, 4, 5, 6) // {x:1}456
var obj = {
a: 1,
method: function (x, y) {
console.log(this.a, x, y);
}
};
obj.method(2, 3); // 123
obj.method.call({a: 4}, 5, 6) // 456
apply 메서드
Function.prototype.apply(thisArg. [argsArray])
apply 메서드는 call 메서드와 기능적으로 완전히 동일하다. apply메서드는 두 번째 인자를 배열로 받아 그 배열의 요소들을 호출할 함수의 매개변수로 지정한다는 점에서만 차이가 있다.
var func = function(a,b,c) {
console.log(this, a, b, c);
}
func.apply({x: 1}, [4, 5, 6]) // {x:1}456
call/apply 메서드 활용
유사배열객체에 배열 메서드를 적용
var obj = {
0: 'a',
1: 'b',
2: 'c',
length: 3
};
Array.prototype.push.call(obj, 'd');
console.log(obj); // {0: 'a', 1: 'b', 2: 'c', 3: 'd', length: 4}
var arr = Array.prototype.slice.call(obj);
console.log(arr); // ['a', 'b', 'c', 'd']
객체에는 배열 메서드를 직접 적용할 수 없다. 그러나 배열의 구조와 유사한 객체의 경우(유사배열객체) call 또는 apply를 이용해 배열 메서드를 차용할 수 있다.
함수 내부에서 접근할 수 있는 arguments 객체도 유사배열객체이므로 위 방법으로 배열로 전환해 활용할 수 있다.
이외에도 querySelectorAll, getElementsByClassName, NodeList 등에 활용 가능하다.
call/apply를 이용해 형변환하는 것은 본래 메서드 의도와는 동떨어진 활용법일 수 있다. ES6에서는 유사배열객체 또는 순회 가능한 모든 종류의 데이터 타입을 배열로 전환하는 Array.from메서드가 도입되었다.
생성자 내부에서 다른 생성자를 호출
생성자 내부에 다른 생성자와 공통된 내용이 있을 경우 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;
}
var by = new Student('보영', 'female', '단국대');
var jn = new Employee('재난', 'male', '구글');
여러 인수를 묶어 하나의 배열로 전달하고 싶을 때 - apply 활용
여러 개의 인수를 받는 메서드에게 하나의 배열로 인수들을 전달하고 싶을 때 apply 메서드를 활용할 수 있다. 예를 들어, 최대/최솟값을 구해야 하는 경우 apply를 사용한다면 더 간단하게 구현 가능하다.
var nums = [10, 20, 3, 16, 45];
var max = min = nums[0];
nums.forEach(function(num) {
if(num > max) {
max = num;
}
if(num < min) {
min = num;
}
})
console.log(max, min);
// apply 활용
var nums = [10, 20, 3, 16, 45];
var max = Math.max(null, nums);
var min = Math.min(null, nums);
bind 메서드
Function.prototype.bind(thisArg[, arg1[, arg2[, ...]]])
ES5에서 추가된 기능으로, call과 비슷하지만 즉시 호출하지는 않고 넘겨 받은 this 및 인수들을 바탕으로 새로운 함수를 반환하기만 하는 메서드다. 즉 bind 메서드는 함수에 this를 미리 적용하는 것과 부분 적용 함수를 구현하는 두 가지 목적을 모두 지닌다.
화살표 함수의 예외사항
화살표 함수는 실행 컨텍스트 생성 시 this를 바인딩하는 과정이 제외됐다. 즉 이 함수 내부에는 this가 없으며, 접근하고자 하면 스코프체인상 가장 가까운 this에 접근하게 된다.(위에서 설명)
var obj = {
outer: function () {
console.log(this); // {outer: f}
var innerFunc = () => {
console.log(this); // {outer: f}
};
innerFunc();
}
};
obj.outer();
별도의 인자로 this를 받는 경우(콜백 함수 내에서의 this)
콜백 함수를 인자로 받는 메서드 중 일부는 추가로 this로 지정할 객체(thisArg)를 인자로 지정할 수 있는 경우가 있다. 이러한 메서드의 thisArg 값을 지정하면 콜백 함수 내부에서 this 값을 원하는 대로 변경할 수 있다. 특히 배열 메서드에 많이 있으며 ES6에서 등장한 Set, Map에도 존재한다.
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, 95);
console.log(report.sum, report.count, report.average()); // 240 3 80
add 메서드에 세 인자(60, 85, 95)를 담아 호출하면 이 세 인자를 배열로 만들어 forEach 메서드가 실행된다. 콜백 함수 내부에서의 this는 add 메서드에서의 this가 전달된 상태이므로 add 메서드의 this(report)를 그대로 가리키고 있다. 따라서 배열의 세 요소를 순회하면서 결과를 반환할 수 있다.
이외에도 콜백 함수와 함께 thisArg를 인자로 받는 다양한 배열 메서드들이 있다.
Array.prototype.forEach(callback, [, thisArgs])
Array.prototype.map(callback, [, thisArgs])
Array.prototype.filter(callback, [, thisArgs])
Array.prototype.some(callback, [, thisArgs])
Array.prototype.every(callback, [, thisArgs])
Array.prototype.find(callback, [, thisArgs])
Array.prototype.findIndex(callback, [, thisArgs])
Array.prototype.flatMap(callback, [, thisArgs])
Array.prototype.from(callback, [, thisArgs])
Set.prototype.forEach(callback, [, thisArgs])
Map.prototype.forEach(callback, [, thisArgs])
Reference
'개발 > 프론트엔드' 카테고리의 다른 글
[JavaScript] 코어 자바스크립트 week5 - 클로저 (1) | 2024.11.18 |
---|---|
[JavaScript] 코어 자바스크립트 week4 - 콜백함수 (0) | 2024.11.11 |
[JavaScript] 코어 자바스크립트 week2 -실행 컨텍스트 (3) | 2024.10.28 |
[JavaScript] 코어 자바스크립트 week1 - 데이터 타입 (3) | 2024.10.22 |
[NextJS] GIF는 Image컴포넌트의 최적화가 지원되지 않는다?! (feat. Cloudinary) (0) | 2024.10.17 |