[JS] 상황에 따른 this 바인딩

Kyle·2020년 11월 19일
10

javascript

목록 보기
5/18

this

this란???
this는 Object를 참조하는 keyword이다.

어떤 Object?

javascript는 함수 호출 방식에 따라서 this가 참조하는 객체가 달라진다.

WEB에서의 this

web에서 전역 객체는 window이다.

console창에 this를 쳐보면 window객체가 나온다.

Node에서의 this

그냥 this를 console에 출력시키면 {} 처럼 빈객체가 출력된다.

window가 아닌 빈 객체가 나오는 이유는 WEB과 노드는 다른 런타임이기 때문에 DOM관련된 객체는 없다.

실제 Node에서의 this는 module.exports이다. 파일을 모듈로 사용할 수 있게 해주는 객체이다. 즉, this, exports, module.exports는 모두 같다.

console.log(this === global); // false
console.log(this === module.exports, this === exports); //true, true

function a() {
  console.log(this); //Object[global]
  console.log(this === exports); //false
}

흔히 우리가 아는 this가 global객체로 바인딩 되는 경우는 따로 있다. this는 함수를 어떻게 선언했는지 어디서 활용됐는지에 따라서 동적으로 혹은 정적으로 정해진다.

함수 호출에 따른 this 바인딩

자바스크립트는 함수 호출 방식에 의해 this에 바인딩될 객체가 결정된다.

함수 실행 컨텍스트에서는 this는 함수가 어떻게 호출됐냐 (how the function is called)에 집중해서 파악하면된다.

아래의 코드를 보고 간단히 이해해보자

const person ={
    name:'kyle',
    sayHello:function(){
        console.log('hello',this.name)
    }
}
person.sayHello() //hello kyle
const callMyName = person.sayHello
callMyName() //hello

이제 함수가 어떻게 호출이 됐느냐(함수선언식의 경우)에 집중해서 아래의 글을 보면 이해가 수월할 것이다.

함수 선언식

함수 선언식 안에서 this는 global객체를 의미한다. 함수선언식은 함수가 호출,실행 될 때 this가 동적으로 결정된다.

이런 이유 때문인지 함수 선언식이 내부함수에서 선언될 경우 (콜백함수, 내부함수의 내부함수)는 항상 global,window 처럼 전역객체에 바인딩된다.

이런 방법을 해결하기 위한 방법

  • this를 사용하려는 객체에서 사전에 let that = this 로 변수로 선언해서 사용하기
  • apply, call, bind 메소드 사용해 this 바인딩 하기
const obj = {
  name: "kyle",
  getName: function () {
    console.log(this); //obj
    const that = this;
    setTimeout(function () {
      console.log(this); // window
      console.log(that); //obj
    });
  },
};

arrow function

arrow function은 this 바인딩할 객체가 선언할 때 정적으로 결정된다. 즉, 언제나 상위 스코프의 this를 가르킨다 이를 Lexical this 라한다.

위 함수 선언식과 같은 예제지만 내부함수임에도 화살표 함수는 상위 스코프의 this를 가르키고 있다. 여기서 상위 스코프는 getName 메소드이다.

잠깐! getName메소드는 함수 선언식이기 때문에 호출하지 않으면 this는 undefined입니다. 아래 코드를 호출했을 때 안했을 때를 구분해서 보세요!

화살표 함수는 콜백함수에서 this를 사용할 때 헷갈리지 않게 사용할 수 있다.

  • getName() 호출 전
let a;
const obj = {
  name: "kyle",
  getName: function () {
    a=this
    console.log(this);
    setTimeout(() => {
      console.log(this); 
    });
  },
};
console.log(this) // 뭘까요~?

/*출력
undefined
*/
  • getName() 호출 후
let a;
const obj = {
  name: "kyle",
  getName: function () {
    a=this
    console.log(this);
    setTimeout(() => {
      console.log(this); 
    });
  },
};
obj.getName()
console.log(this)

/*출력
obj
obj
obj
*/

하지만 arrow function을 조심해야 하는 경우도 있다. addEventListener, 생성자함수 등 있다.
poiemaweb 을 보면 자세하게 나와있다.

생성자 함수를 사용할 때 this 바인딩

생성자 함수는 new연산자를 붙여서 호출해 객체를 생성하는 함수이다. 암묵적으로 맨 앞글자를 대문자로 사용한다.

생성자 함수를 사용할 때 this 바인딩을 예제를 통해 확인해보자

function Person(name) {
  this.name = name;
  console.log(this);
}

const kyle = new Person("kyle"); //Person { name: 'kyle' }
console.log(kyle.__proto__); //Person{}
const kelly = Person("kelly"); // global Object

new 연산자와 생성자 함수를 호출하면

  1. 빈객체를 생성하고 이 객체에 this 바인딩한다.
  2. 빈객체는 생성자함수의 prototype 프로퍼티가 가르키는 객체를 자신의 프로토타입 객체로 설정한다.
  3. 빈 객체에 this를 이용해 프로퍼티, 메소드를 생성해 추가한다.
  4. 객체를 반환한다.

** this외에 다른것을 반환하거나 this를 반환하지 않는 함수는 생성자 함수의 역할을 수행할 수 없다는 것을 알 수 있다.

객체 리터럴 vs 생성자 함수

둘의 차이는 각자의 프로토타입 객체가 다르다.

  • 객체 리터럴 : Object.prototpye
  • 생성자 함수 : 생성자함수.prototype

apply, call, bind

Function.prototype 객체의 메소드인 apply, call bind 를 통해서 this를 특정 객체에 명시적으로 바인딩 할 수 있다.

  • 위 apply,call 메소드에 this를 입력하는 자리에 null을 입력하면 apply,call가 실행된 함수 인스턴스는 전역객체에 바인딩 돼 실행된다고 생각하면된다.

apply

func.apply(thisArg, [argsArray])

apply는 함수를 호출하는 함수이다. 주로 유사 배열 객체들을 객체 메소드를 활용할 때 사용된다.

function k() {
  console.log(arguments); //[Arguments] { '0': 1, '1': 2, '2': 3 }

  console.log(arguments.slice()); // Error

  const arr = Array.prototype.slice.apply(arguments);
  console.log(arr); //[ 1, 2, 3 ]
}

k(1, 2, 3);

apply는 slice메소드를 호출하는데 this는 arguments로 바인딩하라는 뜻이다. 즉, arguments.slice()

call

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

call은 apply와 하는 역할은 같다. 하지만 apply와 문법이 조금 다르다.

apply는 array로 실행시킬 함수의 arguments를 받는 반면 call은 인자를 하나하나 받는다.

bind

bind는 apply와 call과 다르게 함수를 리턴하고 호출하지는 않는다.

const obj = {
  name: "kyle",
  sayHello: function () {
    console.log(this.name);
  },
};

const obj2 = {
  name: "kelly",
};

obj.sayHello(); //kyle
obj.sayHello.call(obj2); //kelly
obj.sayHello.bind(obj2)(); //kelly

위의 예제처럼 bind는 함수를 리턴하기 때문에 호출을 따로 해주어야 한다.

addEventListener 사용시 콜백함수의 this

addEventListener 를 사용해서 콜백 함수를 호출할 때 콜백함수는 addEventLisener를 호출하는 즉, 트리거가 되는 객체가 this 로 바인딩 돼 들어간다.

이러한 현상은 코딩할 때 혼동을 주기 마련이다.

2가지 해결 방안이 있다.

  1. 콜백함수를 화살표함수로 작성하기
  2. bind로 this 바인딩하기

아래 예제를 보고 이해해보자.

class Event {
  init() {
    div.addEventListener("click", this.sayThis);
    div.addEventListener("click", this.sayThat);
    div.addEventListener("click", this.useBind.bind(this));
  }
  sayThis() {
    console.log("함수 선언식", this); // div
  }
  sayThat = () => {
    console.log("화살표 함수", this); //Event
  };
  useBind() {
    console.log("함수 선언식 with bind", this); //Event
  }
}

하지만 화살표 함수를 이용할 때 문제점이 있다.

Event의 prototype property 가 가르키는 객체에 화살표 함수로 선언된 sayThat은 포함되지 않는다.

이는 상속한 뒤 override하고 싶을 때 번거로움을 유발시킨다.

setTimeout

setTimeout와 같은 웹(브라우저)에서만 사용가능한 메소드들은 windowthis가 자동으로 바인딩 된다. 그렇기 때문에 class의 메소드에서 활용할 경우 위의 addEventListener처럼 bind 또는 함수 표현식으로 작성해야 한다.

참조 : https://www.zerocho.com/category/NodeJS/post/5b67e8607bbbd3001b43fd7b

https://poiemaweb.com/js-this

https://stackoverflow.com/questions/39048796/function-declarations-or-expressions-for-class-methods

profile
Kyle 발전기

7개의 댓글

comment-user-thumbnail
2021년 2월 22일

헐 this바인딩 공부좀 하려고 구글에치고 아무거나 보는데..설명 너무좋네~ 생각하고있었는데 카일이셨네용 ㅋㅋㅋ 잘 읽고 갑니다~~~
-지나가던 Jenny-

1개의 답글

ㅋㅋㅋㅋ헐 this바인딩 공부좀 하려고 구글에치고 아무거나 보는데..설명 너무좋네~ -> 제니 저돜ㅋㅋㅋㅋㅋㅋ 둥이 아버님 글 잘보고 갑니다!!

1개의 답글
comment-user-thumbnail
2023년 2월 20일

좋은 글 감사합니다! this에 대해 알아갈수 있었던 시간 이였습니다! 👍🏻

답글 달기
comment-user-thumbnail
2023년 3월 9일

참고가 되었습니다 고맙습니다 :)

답글 달기
comment-user-thumbnail
2024년 1월 26일

getName() 호출 후 아래 예제가 잘못된 것 같네요.
동일한 코드 codepen에서 실행하면 object, window, object순으로 콘솔에 찍혀용

답글 달기