본문 바로가기
CoreJavaScript-study/dil

[DIL] 03 this

by 어느새벽 2024. 7. 10.

01 상황에 따라 달라지는 this

자바스크립트에서 this는 기본적으로 실행 컨텍스트가 생성될 때 함께 결정된다. 실행 컨텍스트는 함수를 호출할 때 생성되므로, 바꿔 말하면 this는 함수를 호출할 때 결정된다고 할 수 있다. 함수를 어떤 방식으로 호출하느냐에 따라 값이 달라지는 것이다. 

 

3-1-1 전역 공간에서의 this

전역 공간에서 this는 전역 객체를 가리킨다. 개념상 전역 컨텍스트를 생성하는 주체가 바로 전역 객체이기 때문이다. 전역 객체는 자바스크립트 런타임 환경에 따라 다른 이름과 정보를 가지고 있다. 브라우저 환경에서 전역객체는 window이고 Node.js 환경에서는 global이다.

 

전역변수를 선언하면 자바스크립트 엔진은 이를 전역객체의 프로퍼티로도 할당한다. 변수이면서 객체의 프로퍼티이기도 한 셈이다.

console.log(this); // window 객체 출력

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인 것이 의아하다. 그 이유는 자바스크립트의 모든 변수는 실은 특정 객체의 프로퍼티로서 동작하기 때문이다. 사용자가 var 연산자를 이용해 변수를 선언하더라도 실제 자바스크립트 엔진은 어떤 특정 객체의 프로퍼티로 인식하는 것이다. 특정 객체란 바로 실행 컨텍스트의 LexicalEnvironment(L.E)이다. 실행 컨텍스트는 변수를 수집해서 L.E의 프로퍼티로 저장한다. 이후 어떤 변수를 호출하면 L.E를 조회해서 일치하는 프로퍼티가 있을 경우 그 값을 반환한다. 전역 컨텍스트의 경우 L.E는 전역객체를 그대로 참조한다.

 

이제 정확히 표현하자면 전역변수를 선언하면 자바스크립트 엔진은 이를 전역객체의 프로퍼티로 할당한다. 그렇다면 window.a나 this.a가 1이 나오는 이유가 설명되는데, a를 직접 호출할 때도 1이 나오는 까닭은 변수 a에 접근하고자 하면 스코프 체인에서 a를 검색하다가 가장 마지막에 도달하는 전역 스코프의 L.E, 즉 전역객체에서 해당 프로퍼티 a를 발견해서 그 값을 반환하기 때문이다. 원리는 이렇지만 단순히 (window.)이 생략된 것이라고 여겨도 된다.

 

전역 공간에서는 var로 변수를 선언하는 대신 window의 프로퍼티에 직접 할당하더라도 결과적으로 var로 선언한 것과 대부분 똑같이 동작한다. 하지만 '삭제' 명령을 할 때에는 다르다.

var a = 1;
window.b = 2;
console.log(a, window.a, this.a); // 1 1 1 
console.log(b, window.b, this.b); // 2 2 2 


var a = 1;
delete window.a; // false
console.log(a, window.a, this.a); // 1 1 1

var b = 2;
delete b; // false
console.log(b, window.b, this.b); // 2 2 2

window.c = 3;
delete window.c; // true
console.log(c, window.c, this.c); // Uncaught ReferenceError : c is not defined

 

예제를 보면 전역객체의 프로퍼티로 할당한 경우에는 삭제가 되는 반면 전역변수로 선언한 경우에는 삭제가 되지 않는 것을 확인할 수 있다. 이는 사용자가 의도치 않게 삭제하는 것을 방지하는 차원에서 마련한 나름의 방어전략이라고 해석된다. 즉 전역변수를 선언하면 자바스크립트 엔진이 이를 자동으로 전역객체의 프로퍼티로 할당하면서 추가적으로 해당 프로퍼티의 configurable 속성(변경 및 삭제 가능성)을 false로 정의하는 것이다.

 

3-1-2 메서드로서 호출할 때 그 메서드 내부에서의 this

가장 일반적으로 함수를 호출하는 방법 두가지로 함수로서 호출하는 경우와 메서드로서 호출하는 경우이다. 함수는 그 자체로 독립적인 기능을 수행하는 반면, 메서드는 자신을 호출한 대상 객체에 관한 동작을 수행한다. 어떤 함수를 객체의 프로퍼티에 할당한다고 해서 그 자체로서 무조건 메서드가 되는 것이 아니라 객체의 메서드로서 호출할 경우에만 메서드로 동작하고, 그렇지 않으면 함수로 동작한다.

 

var func = function (x) {
	console.log(this, x);
}; 
func(1); // window {...} 1 

var obj = {
	method: func
};
obj.method(2); // {method: f} 2

 

1번째 줄에서 func라는 변수에 익명함수를 할당했다. 4번째 줄에서 func를 호출했더니 this로 전역객체 window가 출력된다. 6번째 줄에서 obj라는 변수에 객체를 할당하는데, 그 객체의 method 프로퍼티에 앞에서 만든 func 함수를 할당했다. 이제 9번째 줄에서 obj의 method를 호출했더니, 이번에는 this가 obj라고 한다. obj의 method 프로퍼티에 할당한 값과 func 변수에 할당한 값은 모두 1번째 줄에서 선언한 함수를 참조한다. 즉 원래의 익명함수는 그대로인데 이를 변수에 담아 호출한 경우와 obj 객체의 프로퍼티에 할당해서 호출한 경우에 this가 달라지는 것이다.

 

함수로서 호출과 메서드로서의 호출은 함수 앞에 점(.)이 있는지 여부에 따라 다르다. 점이 없으면 함수로서 호출한 것이고, 점이 있으면 메서드로서 호출한 것이다. 대괄호 표기법에 따른 경우에도 메서드로서 호출한 것이다.

 

메서드 내부에서의 this 

this에는 호출한 주체에 대한 정보가 담긴다. 어떤 함수를 메스드로서 호출하는 경우 호출 주체는 바로 함수명(프로퍼티명) 앞의 객체이다. 점 표기법의 경우 마지막 점 앞에 명시된 객체가 곧 this가 된다.

 

3-1-3 함수로서 호출할 때 그 함수 내부에서의 this 

함수 내부에서의 this

어떤 함수를 함수로서 호출할 경우에는 this가 지정되지 않는다. 위에 실행 컨텍스트를 활성화할 당시에 this가 지정되지 않은 경우 this는 전역 객체를 바라본다고 했다. 따라서 함수에서의 this는 전역 객체를 가리킨다. 

 

메서드 내부함수에서의 this 

해당 함수를 호출하는 구문 앞에 점 또는 대괄호 표기가 있는지 여부를 통해 판단할 수 있다. 메서드 내부 함수에서의 this를 우회하는 방법으로는 변수를 활용하는 것이다.

 

this를 바인딩하지 않는 함수

함수 내부에서 this가 전역객체를 바라보는 문제를 보완하고자 this를 바인딩하지 않는 화살표함수가 새로 도입된다. 화살표 함수는 실행 컨텍스트를 생성할때 this 바인딩 과정 자체가 빠지게 되어, 상위 스코프의 this를 그대로 활용할 수 있다.

그 밖에도 call, apply 등의 메서드를 활용해 함수를 호출할 때 명시적으로 this를 지정하는 방법이 있다.

 

3-1-4 콜백 함수 호출 시 그 함수 내부에서의 this 

함수 A의 제어권을 다른 함수(또는 메서드) B에게 넘겨주는 경우 함수 A를 콜백 함수라 한다. 콜백 함수의 제어권을  가지는 함수(메서드)가 콜백 함수에서의 this를 무엇으로 할지 결정하며, 특별히 정의하지 않은 경우에는 기본적으로 함수와 마찬가지로 전역객체를 바라본다. 

 

3-1-5 생성자 함수 내부에서의 this

생성자 함수는 어떤 공통된 성질을 지니는 객체들을 생성하는 데 사용하는 함수이다. 객체지향 언어에서는 생성자를 클래스, 클래스를 통해 만든 객체를 인스턴스라고 한다. 자바스크립트는 함수에 생성자로서의 역할을 함께 부여했다. new 명령어와 함께 함수를 호출하면 해당 함수가 생성자로서 동작하게 된다. 그리고 어떤 함수가 생성자 함수로서 호출된 경우 내부에서의 this는 곧 새로 만들 구체적인 인스턴스 자신이 된다. 생성자 함수를 호출하면 우선 생성자의 prototype 프로퍼터를 참조하는 __proto__라는 프로퍼티가 있는 객체(인스턴스)를 만들고, 미리 준비된 공통 속성 및 개성을 해당 객체(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}

 

Cat이란 변수에 익명 함수를 할당했다. 이 함수 내부에서는  this에 접근해서 bark, name, age 프로퍼티에 각각 값을 대입한다. 6번째와 7번째 줄에서는 new 명령어와 함께 Cat 함수를 호출해서 변수 choco, nabi에 각각 할당했다. 8번째 줄에서 choco와 nabi를 출력해보니 각각 Cat 클래스의 인스턴스 객체가 출력된다. 즉 6번째 줄에서 실행한 생성자 함수 내부에서의 this는 choco 인스턴스를, 7번째 줄에서 실행한 생성자 함수 내부에서의 this는 nabi 인스턴스를 가리킴을 알 수 있다.

 

02 명시적으로 this를 바인딩하는 방법

3-2-1 call 메서드

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로 지정할 수 있다.

 

3-2-2 apply 메서드

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

 

3-2-3 call / apply 활용 예제 (생략)

 

3-2-4 bind 메서드

Function.prototype.bind(thisArg[, arg1[, arg2[, ...]]])

 

bind 메서드는 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

 

6번째 줄에서 bindFunc1 변수에는 func에 this를 {x:1}로 지정한 새로운 함수가 담긴다. 이제 7번째 줄에서 bindFunc1을 호출하면 원하는 결과를 얻을 수 있게 된다. 한편 9번째 줄의 bindFunc2 변수에는 func에 this를 {x:1}로 지정하고, 앞에서부터 두 개의 인수를 각각 4, 5로 지정한 새로운 함수를 담았다. 이후 10번째 줄에서 매개변수로 6, 7을 넘기면 this 값이 바뀐 것을 제외하고는 최초 func 함수에 4, 5, 6, 7을 넘긴 것과 같은 동작을 한다. 11번째 줄에서도 마찬가지다. 6번째 줄의 bind는 this만을 지정한 것이고, 9번째 줄의 bind는 this 지정과 함께 부분 적용 함수를 구현한 것이다.

 

name 프로퍼티

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

 

3-2-5 화살표 함수의 예외사항

새롭게 도입된 화살표 함수는 실행 컨텍스트 생성 시 this를 바인딩하는 과정이 제외됐다. 즉 이 함수 내부에는 this가 아예 없으며, 접근하고자 하면 스코프체인상 가장 가까운 this에 접근하게 된다.

var = obj = {
	outer: function () {
    	console.log(this);
        var innerFunc = () => {
        	console.log(this);
    	};
        innerFunc();
	}
};
obj.outer();

 

3-2-6 별도의 인자로 this를 받는 경우(콜백 함수 내에서의 this)

콜백함수를 인자로 받는 메서드 중 일부는 this로 지정할 객체(thisArg)를 인자로 추가하여 지정할 수 있는 경우가 있다.  이러한 메서드의 thisArg 값을 지정하면 콜백 함수 내부에서 this 값을 원하는 대로 변경할 수 있다. 이런 형태는 여러 내부 요소에 대해 같은 동작을 반복 수행해야 하는 배열 메서드에 많이 포진돼 있으며, 같은 이유로 새로 등장한 Set, Map 등의 메서드에도 일부 존재한다.

var report = {
	sum: 0,
    count: 0,
    add: function () {
    	var args = Array.prototype.slice.call(arguments);
        args.forEach(function (entry) {
        	this.sim += 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 메서드가 있다. 5번째 줄에서 add 메서드는 arguments를 배열로 변환해서 args 변수에 담고, 6번째 줄에서는 이 배열을 순회하면서 콜백 함수를 실행하는데, 이때 콜백 함수 내부에서의 this는 forEach 함수의 두번째 인자로 전달해준 this(9번째 줄)가 바인딩된다. 11번째 줄의 average는 sum 프로퍼티를 count 프로퍼티로 나눈 결과를 반환하는 메서드이다.

15번째 줄에서 60, 85, 95를 인자로 삼아 add 메서드를 호출하면 이 세 인자를 배열로 만들어 forEach 메서드가 실행된다. 콜백 함수 내부에서의 this는 add 메서드에서의 this가 전달된 상태이므로 add 메서드의 this(report)를 그대로 가리키고 있다. 따라서 배열의 세 요소를 순회하면서 report.sum 값 및 report.count 값이 차례로 바뀌고, 순회를 마친 결과 report.sum에는 240이, report.count에는 3이 담기게 된다.

 

03 정리

다음 규칙은 명시적 this 바인딩이 없는 한 늘 성립한다.

 

  • 전역공간에서의  this는 전역객체(브라우저에서는 window, Node.js에서는 global)를 참조한다.
  • 어떤 함수를 메서드로서 호출한 경우 this는 메서드 호출 주체(메서드명 앞의 객체)를 참조한다.
  • 어떤 함수를 함수로서 호출한 경우 this는 전역객체를 참조한다. 메서드의 내부함수에서도 같다.
  • 콜백 함수 내부에서의 this는 해당 콜백 함수의 제어권을 넘겨받은 함수가 정의한 바에 따르며, 정의하지 않은 경우에는 전역객체를 참조한다.
  • 생성자 함수에서의 this는 생성될 인스턴스를 참조한다.

 

다음은 명시적 this 바인딩이다. 위 규칙에 부합하지 않는 경우에는 다음 내용을 바탕으로 this를 예측할 수 있다.

 

  • call, apply 메서드는 this를 명시적으로 지정하면서 함수 또는 메서드를 호출한다.
  • bind 메서드는 this 및 함수에 넘길 인수를 일부 지정해서 새로운 함수를 만든다.
  • 요소를 순회하면서 콜백 함수를 반복 호출하는 내용의 일부 메서드는 별도의 인자로 this를 받기도 한다.

'CoreJavaScript-study > dil' 카테고리의 다른 글

[DIL] 06 프로토타입  (4) 2024.07.22
[DIL] 05 클로저  (2) 2024.07.15
[DIL] 04 콜백 함수  (1) 2024.07.13
[DIL] 02 실행 컨텍스트  (0) 2024.07.04
[DIL] 01 데이터타입  (0) 2024.07.02