TIL-51 JavaScript This

PRB·2021년 11월 10일
0

JavaScript

목록 보기
18/24
post-thumbnail

자바스크립트의 this는 어디서든 사용할 수 있다.
상황에 따라 this가 바라보는 세상이 달라지는데, 어떤 이유로 그렇게 되는지를 파악하기 힘든 경우도 있고 예상과 다르게 엉뚱한 대상을 바라보는 경우도 있다.

함수와 객체(메서드)의 구분이 느슨한 자바스크립트에서 this는 실질적으로 이 둘을 구분하는 거의 유일한 기능이다.

1. 상황에 따라 달라지는 this

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

1. 전역 공간에서 this

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

전역 공간에서 this(브라우저 환경)

console.log(this)  // {alert: f(), atob: f(), ...}
console.log(window)  // {alert: f(), atob: f(), ...}
console.log(this === window); // true

전역 공간에서 this(Node.js 환경)

console.log(this)  // { process : {title: 'node' ...} }
console.log(global)  // { process : {title: 'node' ...} }
console.log(this === global); // true
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인 것이 의아하다.
그 이유는 자바스크립트의 모든 변수는 실은 실행 컨텍스트의 LexicalEnvironment의 프로퍼티로서 동작하기 때문이다. 사용자가 var 연산자를 이용해 변수를 선언하더라도 실제 자바스크립트 엔진은 어떤 특정 객체의 프로퍼티로 인식하는 것이다. 이후에 어떤 변수를 호출하면 LexicalEnvironment를 조회해서 일치하는 프로퍼티가 있을 경우 그 값을 반환합니다.

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

그렇다면 window.a나 this.a가 나오는 이유를 알겠는데 a를 직접 호출하였을 때도 1이 나오는 이유는 무엇일까?
이유는 변수 a에 접근하고자 하면 스코프 체인에서 a를 검색하다가 가장 마지막에 도달하는 전역 스코프의 LexicalEnvironment, 즉 전역 객체에서 해당 프로퍼티 a를 발견해서 그 값을 반환하기 때문이다. 원리는 이렇지만 그냥 단순하게 window. 이 생략된 것이라고 생각해도 된다.

2. 메서드로서 호출할 때 그 메서드 내부에서의 this

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

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

obj의 method 프로퍼티에 할당한 값과 func 변수에 할당한 값은 모두 1번째 줄에서 선언한 함수를 참조한다. 즉 원래의 익명 함수는 그대로인데 이를 변수에 담아 호출한 경우와 obj 객체의 프로퍼티에 할당해서 호출한 경우에 this가 달라지는 것이다.

함수로서 호출과 메서드로서 호출을 어떻게 구별할까?
함수 앞에 점(.)이 있는지 여부만으로 구분할 수 있다. (대괄호 표기법에 따른 경우에도 메서드로서 호출한 것이다.)

1. 메서드 내부에서의 this

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

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

1. 함수 내부에서의 this

어떤 함수를 함수로서 호출할 경우에는 this가 지정되지 않는다.
this에는 호출한 주체에 대한 정보가 담긴다. 그런데 함수로서 호출하는 것은 호출 주체(객체지향언어에서의 객체)를 명시하지 않고 개발자가 코드에 직접 관여해서 실행한 것이기 때문에 호출 주체의 정보를 알 수 없는 것이다. 실행 컨텍스트를 활성화 당시에 this가 지정되지 않는 경우 this는 전역 객체를 바라본다.
따라서 함수에서의 this는 전역 객체를 가리킨다.

2. 메서드 내부에서의 this

var obj1 = {
	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

this 바인딩에 관해서는 함수를 실행하는 당시의 주변 환경(메서드 내부인지,, 함수 내부인지 등)은 중요하지 않고, 오직 해당 함수를 호출하는 구문 앞에 점 또는 대괄호 표기가 있는지 없는지가 관건이다.

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

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

4. 콜백 함수 호출 시 그 함수 내부에서의 this

콜백 함수란?
함수 A의 제어권을 다른 함수(또는 메서드) B에게 넘겨주는 경우 함수 A를 콜백 함수라고 한다.

콜백 함수도 함수이기 때문에 기본적으로 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): 전역 객체, (2): 전역 객체, (3): 버튼

5. 생성자 함수 내부에서의 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는 생성될 인스턴스를 참조한다.

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

1. call 메서드

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
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

2. apply 메서드

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

bind 메서드는 ES5에서 추가된 기능으로, call과 비슷하지만 즉시 호출하지는 않고 넘겨 받은 this 및 인수들을 바탕으로 새로운 함수를 반환하기만 하는 메서드이다.

profile
사용자 입장에서 사용자가 원하는 것을 개발하는 프론트엔드 개발자입니다.

0개의 댓글