본문 바로가기

JavaScript

[JavaScript] this에 관한 탐구

this 니가 뭔데 날 힘들게 해..

🥲 내가 알고 있던 this는?

this는 일반 함수 내에서 사용 시 전역 객체를 가리키고, 화살표 함수에선  함수가 포함된 상위 환경의 this를 가리킨다.

라는 게 내가 알고 있는 this의 정의다.  

 

자바스크립트를 공부하면서 어려웠던 개념 중 하나가 this 였고, 이번에 공부하면서 익힌 개념을 글로 정리해본다.

🐝 실행 컨텍스트

먼저 this에 대해 시작하기 전에 this는 실행컨텍스트와 연관되어 있어서 실행컨텍스트에 관해 간단히 설명하고자 한다.

 

 

실행 컨텍스트 안에 ThisBinding이 있는데 이 ThisBinding은 실행 컨텍스트가 활성화될 때 한다! 하는 걸 알고 있기만 하면 된다.

 

그렇다면 실행 컨텍스트는 언제 생성될까?

=> 해당하는 함수가 호출되는 순간 생성된다!

=> this는 함수를 호출하는 방식에 따라 달라지겠구나

 

그렇다면 this를 호출하는 방식에는 어떤 것이 있을까?

 

크게 다섯 가지로 나누어서 정리하고자 한다.

- 전역 공간에서의 this

- 함수로서 호출할 때의 this

- 메서드로서 호출할 때의 this

- 콜백 함수로서 호출 할 때의 this

- 생성자 함수로서 호출 할 때의 this

✔️ 전역 공간에서의 this

=>  전역 객체

 

전역 공간에서의 this는 전역 객체이다. 전역 컨텍스트를 실행하는 주체가 전역 객체이기 때문이다.

브라우저 환경에서는 window, 노드 환경에서는 global을 가리킨다.

 

// window 환경
console.log(this); // { alert: f(), atob: f(), blur: f(), btoa: f(), ... }
console.log(window); // { alert: f(), atob: f(), blur: f(), btoa: f(), ... }
console.log(this === window); // true
// node 환경
console.log(this); // { process: { title: 'node', version: 'v10.13.0',... } }
console.log(global); // { process: { title: 'node', version: 'v10.13.0',... } }
console.log(this === global); // true

✔️ 함수로서 호출할 때의 this

=>  전역 객체

 

두 번째로 함수 호출 시 this인데 이때도 this는 전역 객체를 가리킨다.

함수를 실행하는 순간에 실행하는 주체가 전역 공간에서 호출한 것이기 때문이다.

 

// 예제 1
function a() {
  console.log(this); // window
}
a(); 

// 예제 2
function b() {
  function c() {
    console.log(this); // window
  }
  c(); 
}
b();

 

예제 1을 보자. 함수 a를 호출하는 주체가 전역 공간이기 때문에 전역 객체가 출력된다.

하지만 예제 2를 보면 조금 이상함을 느낄 수도 있다.

함수 c는 함수 b 안에서 호출된 것이기 때문에 호출한 주체가 b이지 않나? 싶은데 출력되는 결과는 역시나 window이다.

이 부분을 자바스크립트의 실수라고 하는 사람들도 있고, 자바스크립트 고유의 특성이라고 봐야 할지 의견이 분분한데,

이 의견을 수렴해서 나온 게 ES6 버전의 화살표 함수이다.

 

화살표 함수는 디스 바인딩을 하지 않고 바로 위 컨텍스트에 있는 this를 그대로 가져다 쓴다.

따라서 ES6 환경에서는 이 문제가 해결이 되지만, ES5 환경에서는 이 문제를 해결하기 위해 call이나 apply를 통해 개발자가 this를 지정해 줄 수 있다.

✔️ 메서드로서 호출할 때의 this

=>  메서드 호출 주체 (메서드명 앞)

 

this를 메서드로 호출했을 때는 메서드를 호출한 주체, 즉 누가 호출했는지를 확인하면 된다.

간단하게 메서드 명의 점 바로 앞에 있는 것이 this가 된다.

 

// 예제 1
var obj = {
  methodA: function() {
    console.log(this);
  },
  inner: {
    methodB: function() {
      console.log(this);
    },
  },
};

// 예제 2
obj.methodA(); // { methodA: f, inner: {...} }    ( === obj)
obj['methodA'](); // { methodA: f, inner: {...} } ( === obj)
obj.inner.methodB(); // { methodB: f }            ( === obj.inner)
obj.inner['methodB'](); // { methodB: f }         ( === obj.inner)
obj['inner'].methodB(); // { methodB: f }         ( === obj.inner)
obj['inner']['methodB'](); // { methodB: f }      ( === obj.inner)

 

예제 코드를 보면 obj.methodA()를 호출하면 methodA앞에 있는 obj가 this가 된다.

따라서 obj가 출력된 것을 확인할 수 있다.

 

예제 2를 보자

같은 방식이지만 대괄호 표기법을 썼는데,. 은 대괄호 표기법으로도 표현할 수 있으니

 

obj.methodA(); // { methodA: f, inner: {...} }    ( === obj)
obj['methodA'](); // { methodA: f, inner: {...} } ( === obj)

 

두 코드의 결과는 같다.

같은 방식으로

 

obj.inner.methodB(); // { methodB: f }            ( === obj.inner)
obj.inner['methodB'](); // { methodB: f }         ( === obj.inner)
obj['inner'].methodB(); // { methodB: f }         ( === obj.inner)
obj['inner']['methodB'](); // { methodB: f }      ( === obj.inner)

 

이 코드의 결과도 다 같은 결과라고 볼 수 있다.

✔️ 콜백 함수로서 호출할때의 this

=>  (기본적으로는) 함수의 this

=> 제어권을 가진 함수가 콜백의 this를 지정해둔 경우도 있다

=> 개발자가  this를 바인딩해서 콜백을 넘길 수 있다.

 

콜백 함수에서의 this는 기본적으로 함수에서의 this와 같다. 기본적이란 말은 기본적이지 않는 경우도 있다는 건데,

먼저 첫 번째 예제를 보자

 

var callback = function(){
  console.dir(this) // window
}
var obj = {
  a: 1,
  b: function(cb){
    cb()
  }
}
obj.b(callback)

 

이 예제는 obj객체 안에 b함수가 메서드로서 존재하고, obj.b()라고 callback이라는 함수를 넘겨서 호출하고 있다.

obj.b()는 메서드로서 호출했고 인자로 callback을 넘겼고 여기서 넘어온 함수가 cb()로 실행된다.

이때 이 결과가 console로 찍히는데 결과가 어떻게 될까?

결과적으로 함수로서 호출했으니 전역 객체가 출력된다.

 

두 번째 예제를 보자

 

var callback = function(){
  console.dir(this) // a: 1 
                    // b: ƒ (cb)
                    // [[Prototype]]: Object
}
var obj = {
  a: 1,
  b: function(cb){
    cb.call(this)
  }
}
obj.b(callback)

 

첫 번째 예제와 비슷하지만 cb를 그냥 호출한 게 아니라 call 메서드를 통해 this를 넘겼다. 

b컨텍스트에서의 this는 obj 고 이를 call메서드를 통해 명시적으로 this를 넘겨줬기 때문에 obj가 출력된 것이다.

 

그렇다면 call 메서드는 뭐지?

 

    func.call(thisArg[, arg1[, arg2[, ...]]])

 

call 메서드의 api 문서에 있는 설명이다.

설명을 덧붙이자면 대괄호[] 안에 있는 arg1.. 등은 생략이 가능한 매개변수 이고, 첫 번째 매개변수인  thisArg가 this로 반환하게 된다.

예시 코드를 보자

 

function a(x,y,z){
  console.log(this, x, y, z) // {bb: 'bbb'} 1 2 3
}
var b = {
  bb: "bbb"
}

a.call(b,1,2,3)

 

call 메서드를 써서 인자로 b,1,2,3을 넣어 호출하니 결과가 b인 것을 확인할 수 있다.

따라서 call은 명시적으로 this를 바인딩해주는 역할을 한다. 비슷한 메서드로는 apply, bind 가 있다.

call메서드의 역할을 확인하고 다시 두 번째 예제를 보니 이해가 확 간다.

 

다음은 setTimeout 콜백 함수를 확인해 보자.

 

var callback = function(){
  console.dir(this) // window
}

var obj = {
  a: 1
}

setTimeout(callback, 100)

 

setTimeout 함수는 두 번째 인자로 정한 시간 뒤에 콜백 함수를 실행하는 함수이다.

예제 코드라면 0.1초 뒤에 콜백함수를 실행하는데, 결과는 window가 나오게 된다.

따라서 setTimeout 함수는 this를 별도로 처리하고 있지 않다는 것을 알 수 있다.

 

만약 setTimeout 함수를 써서 this를 개발자가 원하는 값으로 지정하려면 bind메서드를 쓸 수 있다.

 

var callback = function(){
  console.dir(this) // a: 1
}

var obj = {
  a: 1
}

setTimeout(callback.bind(obj), 100)

 

bind의 첫 번째 인자로 원하는  this의 값을 넘겨주었고, this의 결과가 a: 1 이 출력된 것을 확인할 수 있다.

 

다른 예제를 한번 더 확인해보자.

 

document.body.innerHTML += "<div id="a">클릭하세요</div>";

document.getElementById("a").addEventListener("click", function(){
  console.dir(this)   // div#a
});

 

이 예제는 이벤트 핸들러인데 여기서는 this는 뭘까?

setTimeout함수처럼 별도로 정해놓은 this가 없다면 전역 객체가 나오는데 맞을 것이다.

하지만 결과는 html DOM 엘리먼트가 나오게 된다.

여기서 알 수 있는 것은 addEventListener라는 함수는 this를 처리할 때 이벤트가 발생한 타깃 대상 엘리먼트로 지정해 놓은 것을 알 수 있다.

 

따라서 정리를 해 보자면

 

콜백 함수는 기본적으로 함수의 this와 같다.

하지만 제어권을 가진 함수(addEventListener의 경우)는 this를 지정해 둔 경우도 있다.

 

그때그때 다른 콜백함수의 this

 

그때그때 다른 콜백 함수의 this

✔️ 생성자 함수로서 호출할 때의 this

=>  (기본적으로는) 함수의 this

 

생성자 함수란 뭘까?

생성자 함수는 new 연산자를 썼을 때를 말한다.

 

function User(name, age) {
  this.name = name;
  this.age = age;
}

 

이런 식으로 말이다.

 

그렇다면 생성자 함수로서 호출한 경우에는 결과가 어떻게 나올까?

 

function User(name, age) {
  this.name = name;
  this.age = age;
}
var summer = new User("여름", 20)
console.log(summer)  // User {name: '여름', age: 20}

 

생성자 함수로서 호출한 경우는 summer라고 하는 변수, 즉 새로 생성될 User의 인스턴스 객체 자신이 곧 this가 되기 때문에 객체가 새로 만들어지면서 객체 안에 name 프로퍼티, age프로퍼티가 생성되며 출력된다.

 

마치며

this를 공부하면서 처음에는 이게 뭔가? 싶었다...

때에 따라 마음대로 출력되고 그때그때 다르고 혼돈의 도가니탕이었지만 이번 정리를 통해 this에 대한 개념을 잡고 넘어갈 수 있었다.

this는 언제 정해지냐? 함수를 어떤 식으로 호출했는지 확인하면 된다!