What is 'this'?

mr.ginger·2021년 10월 4일
0

This

실행컨텍스트에서 나온 this 키워드에 대하여 알아 볼 차례다.
영어 단어로서 this는 모두가 알다시피 '이것'이라는 의미를 가진 단어이지만, 프로그램상에서는 약간 다르게 사용된다.

Java등 다른 프로그램 언어에서 this는 인스턴스 자신을 가리키는 의미로서 사용된다.
즉 클래스에서만 사용 할 수 있기때문에 크게 혼란을 가지지 않고 사용 할 수 있다.

하지만 자바스크립트에서는 그것과 다른 방식으로 작용하며, 사용된다.
여러곳에서 사용 되며, 상황에 따라 값이 달라지기에 어떤 이유로 달라지고, 어디를 바라보고 있는지 이해하기 힘든 경우가 많다.
그렇다면, 어떻게 다른지 한번 확인 해 보도록 하자.

상황에 따라 달라지는 this

사실 이 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

변수 a를 할당해서 출력했지만 어째서인지 window, this 모두 a라는 프로퍼티가 생기고, 그 값이 1로 출력이 되었다.
이 현상에 대한 이유는, 자바스크립트의 모든 변수는 특정 객체의 프로퍼티로서 동작하기 때문에, 전역에서 변수 a를 초기화 해서 1을 할당함과 동시에, 전역객체인 window에 a라는 프로퍼티가 추가 되고, a에 1이 할당 되었기 때문이다.

여기서 특정 객체란 실행컨텍스트의 LexicalEnvironment(이하 LE)를 뜻한다.
실행컨텍스트는 각 변수에 대한 정보를 수집하여 LE의 프로퍼티로 저장하는데, 이후 변수를 호출하면 LE의 프로퍼티를 참조하여 일치하는 값이 있으면 그 값을 반환한다.
전역컨텍스트의 경우엔 전역객체를 그대로 참조하게 된다.
(정확하게는 GlobalEnv가 전역객체를 참조하고, 전역컨텍스트의 LE가 이 GlobalEnv를 참조하게 된다.)

때문에, 전역에서 var등으로 변수를 선언하지 않고, window에 직접 프로퍼티를 할당해도 대부분의 경우엔 같은 동작을 하게 된다.

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

window.a = 3;
b = 4
console.log(a, window.a, this.a); // 3 3 3
console.log(b, window.b, this.b); // 4 4 4

다만 여기는 예외가 있는데, 바로 delete를 사용 할 때이다.

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

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

위와 같은 결과를 확인 할 수 있다.

여기서 흥미로운 사실은, 전역객체에 프로퍼티로서 추가한 값은 delete로 삭제가 되는데, 변수로 선언한 값은 삭제가 되지 않는 것을 알 수 있다.
이는 사용자가 의도치 않게 변수를 삭제하는것을 막는 차원에서 기능한다고 생각 되어진다.
전역변수를 선언하면 자바스크립트 엔진이 자동으로 전역객체에 프로퍼티로 할당하고, 추가적으로 해당 프로퍼티듸 configurable 속성을 false로 정의 하게 된다.

이처럼 일반적으로 선언한 변수와 전역객체에 직접 프로퍼티를 할당하는것은 호이스팅여부 및 configurable에서 차이를 보이게 된다.

메서드로서의 호출, 메서드 내부에서의 this

들어가기 전에 우선, 함수와 메서드가 어떤 차이가 있는지 알아야 할 필요가 있다.

어떤 함수를 실행하는 방법으로는 함수로서 호출하는 방법과, 메서드로서 호출하는 경우가 있다.
함수와 메서드는 미리 정의한 동작을 수행하는 코드뭉치라는 공통점은 존재하지만, 이 둘을 나누는 차이는 바로 독립성에 있다.

함수는 그 자체로 독립적으로 호출하거나 선언 할 수 있지만, 메서드는 자신을 호출한 대상 객체에 관한 동작을 수행한다.
자바스크립트는 상황별로 this 키워드에 다른 값을 부여하게 함으로써 이를 구현하였다.

흔히 메서드를 '객체의 프로퍼티로 할당 된 함수'로 이해하는 경우가 많지만, 메서드는 객체의 메서드로 호출 될 때에만 메서드로 동작하고, 그 이외는 함수로 동작하게 된다.

메서드로서 호출 하는 방법은 간단하다.

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

var obj = {
  method: func
};

obj.method(1);    // { method : f } 1

위의 예제에서 func(1)부분은 함수로서 호출 되었다.
반면 아래의 obj.method(1)부분은 메서드로 호출 되었다.

메서드로서 호출은 객체의 프로퍼티로 접근하는 방식으로 호출하게 된다.
예를 들어 obj.prop(x) 혹은 obj['prop']과 같은 방식으로 호출 할 수 있다.
보다 간단하게 말하자면, 앞에 해당 메서드가 소속 되어있는 객체가 명시되어 있는 경우는 메서드, 그렇지 않은 경우는 일반 함수로서 호출되었다 볼 수 있다.

그렇다면 메서드 내부에서 this는 어떻게 작용할까?
this는 호출한 주체에 따라 내부에 담기는 값이 변하게 된다.
메서드의 경우, 호출한 주체는 해당 메서드가 속한 객체가 된다.

즉 메서드에서의 this는 메서드가 속한 객체가 되는것이다.
예를 들어 위의 경우를 볼때, obj.method(1)에서 this는 바로 obj인 것이다.

함수로서의 호출, 함수 내부에서의 this

그렇다면 메서드가 아닌 경우는 어떨까?
일반 함수의 경우 위의 func(1)처럼 앞에 해당 함수가 어디에 속하는지 나타나 있지 않다.
그럼 일반 함수의 this는 없는것일까?

그렇지 않다.
실행컨텍스트에서 컨텍스트를 활성화 할 당시에, this가 정해져 있지 않다면 자동적으로 this는 전역객체를 바라보게 된다. 따라서 함수의 this는 곧 전역객체를 가리키게 된다.

그렇다면 메서드 내부에서 함수를 정의하고 실행하면, this는 어디를 바라보게 될까?

var name = 'lee';

var user = {
	name: 'kim',
	getName: function() {
		console.log(this.name);       // (1)
	
		var inner = function() {
		console.log(this.name);       // (2)
		}

		inner();
	}
}

user.getName();

위와 같은 경우에서, (1)과 (2)의 this.name은 어떤 값이 나오는지 유추해 보도록 하자.

우선, 지금까지의 내용을 가볍게 복습해보자.

  • 메서드의 경우, this는 해당 메서드가 속한 객체가 this의 대상이 된다.
  • 일반함수의 경우, 전역객체가 this의 대상이 된다.

이 두가지를 생각하며 위의 예제코드를 다시 한번 읽어보자.

위 코드에서 (1)의 this는 메서드로서 선언된 함수의 this이다.
때문에, (1)에서 출력 되는 값은 'kim'이다.

그렇다면 (2)의 경우도 똑같이 'kim'일까?
(2)의 경우엔 'lee'가 출력 되게 된다.
메서드 안에서 선언 된 함수라도, 일반함수이기에 그 this는 전역객체를 바라보게 된다.
때문에, 전역변수 name로 선언 되어 있는 'lee'가 출력 되게 되는것이다.

그렇다면 메서드 안에서 'lee'가 아닌 'kim'을 출력 할 수는 없는걸까?
메서드 내부에 함수를 만들어도 그 this는 전역을 바라보고 있으니, 만약 메서드 내부에 정의한 함수에서 this를 사용 하고 싶을때 문제가 생길 수 있다.

그런 경우엔 변수를 활용하면 된다.

var name = 'lee';

var user = {
	name: 'kim',
	getName: function() {
		console.log(this.name);       // (1)
	
       		var self = this;
		var inner = function() {
		console.log(self.name);       // (2)
		}

		inner();
	}
}

user.getName();

위와 같이 self라는 변수를 사용해서 this의 값을 저장하면, (2)의 결과도 'kim'을 출력하도록 할 수 있다.
이 self라는 변수명은 정해진 변수명은 아니기에, 사람마다 this, that, 등 다양한 변수명을 사용하는데, 보편적으로 self가 가장 많이 사용 된다고 알려져 있다.

this를 바인딩하지 않는 함수

우리는 메서드 내에서 선언한 함수의 this가 메서드와 상관 없는 전역객체를 바라보고 있다는 사실을 알게 되었다.
변수를 사용해서 보완 할 수는 있지만 꽤나 번거로운것도 사실이기에, ES6부터 도입된 문법중 이 문제를 해결하기 위한 요소가 추가 되었다.

그것이 바로 화살표 함수(Arrow function)이다.
우리가 익히 알고 있는 화살표 함수는

const func = (x) => console.log(x);

func(1);

위 처럼 함수를 간단히 표시 할 수 있고, 함수표현식을 보다 간단하게 사용 할 수 있는 장점이 있는 함수이다.
이 화살표 함수와 this는 꽤나 밀접한 연관이 있다.

화살표 함수는 실행컨텍스트를 생성할때, this바인딩 과정 자체가 생략되어, 함수 자체에는 this가 없고, 가장 가까운 스코프체인의 this를 대상으로 지정하게 된다.

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

콜백함수 내부에서의 this는 기본적으로 콜백함수의 제어권을 가져가는 함수의 this를 따라가게 된다.
콜백함수에 대해 잠깐 설명하자면, 함수 A의 제어권을 다른 함수인 B에게 넘겨주는 경우, 함수 A를 콜백함수라 명명하게 된다.

여기서 제어권이란, 어떤 시점에서 어떤 인자를 넘길 것인지를 결정하는 것을 의미한다.
이때, 함수 A는 함수 B의 로직에 따라 실행되고, this도 함수 B의 내부로직에 따라 결정 되게 된다.
일반적으로 콜백함수로 넘겨지는 함수도 일반함수이기에, 콜백함수의 this는 전역객체를 바라보게 된다.
하지만 제어권을 받은 함수에서 콜백함수에 별도로 this를 지정한 경우, this는 그 대상을 바라보게 된다.

예를 들면, addEventListener 메서드는 콜백함수를 호출할때, 자신의 this를 상속시키도록 정의 되어 있다. 이처럼 특별히 this를 변경하도록 지정 되지 않은 한, 콜백함수의 this는 전역객체를 바라보게 된다.

생성자 함수 내부에서의 this

객체지향 언어에서 생성자는 class, class를 통해 만들어진 대상을 instance라고 한다.
class에 대해 간단하게 설명하자면 흔히 class의 설명으로 사용 되는 붕어빵 틀을 이야기 할 수 있다.

간단히만 설명하고 넘어가자면, class는 붕어빵을 만드는 틀이라 볼 수 있고, instance는 붕어빵 틀로 만들어진 붕어빵에 비유 할 수 있다.

자바스크립트의 경우 함수에 생성자의 역할을 함께 부여하였다.
우리가 Date등을 사용하기 위해 사용하는 명령어 new와 함께 함수를 호출하면, 해당 함수는 생성자로서 동작하게 되는 것이다. 그리고, 어떤 함수가 생성자 함수로서 호출된 경우 내부에서의 this는 생성자로 인해 만들어지는 instance 자신이 된다. 즉, 새로 만들어지는 함수 자신이 this가 되는 것이다.

이 생성자의 호출과 this의 부여는 추후에 포스팅으로 다루도록 하겠다.

예시를 보며 확인을 해보자.

let Food = function(name,price) {
  this.name = name;
  this.price = price;
};

let pizza = new Food('Pizza',25000);

console.log(pizza); // Food {
                    //   name: 'Pizza',
                    //   price: 25000,
                    //   __proto__: { constructor: ƒ Food() }
                    // }

위의 예시와 같이, 생성자 함수로 호출 된 함수 Food의 this는 pizza instance를 가리킴을 알 수 있다.


해당 포스팅은 코어자바스크립트 3장 this를 바탕으로 포스팅하였습니다.

0개의 댓글