[Javascript] this가 this가 아니라고??

DongHwan·2021년 9월 11일
2

nodejs

목록 보기
11/11

Java에서의 this나 python에서의 self는 명확하게 인스턴스 자신을 나타낸다. 객체 내에서 this를 씀으로써 자신의 인스턴스 변수나 함수에 접근을 할 수 있다.

그런데 자바스크립트의 this는 다른 언어들과는 다른 특성을 지닌다. 자바스크립트는 this에 꼭 자기 자신이 들어간다는 법이 없다. 함수를 호출하는 방식에 따라 동적으로 this에 바인딩되는 객체가 달라진다!

이번 게시글에서 이 this에 대해 정리해보고자 한다.

🤔 this란 무엇인가?

this는 함수가 호출되는 시점의 실행 컨텍스트(Execution Context)이다. 실행 컨텍스트는 JS에서 핵심적인 원리 중 하나이다. 그렇기에 여기서 다루기엔 복잡한 내용이라 간단히 설명하자면, 실행 컨텍스트는 코드가 실행되는 환경, 실행하기 위해 필요한 환경이다. 이말을 다르게 말하면, 함수를 실행하는 환경(호출하는 방법, 환경)이 달라지면 this도 변한다는 것이다.

✨ 함수의 호출방식에 따른 this

this는 함수를 어떻게 호출하냐에 따라 다르게 할당된다. this가 달라지는 함수의 호출방식은 다음 4가지이다.

  1. 기본적인 함수 호출(default binding)
  2. 객체의 메소드 호출(암시적 바인딩)
  3. call/apply/bind로 명시적인 바인딩을 하며 호출(명시적 바인딩)
  4. 생성자(new) 함수 호출(new 바인딩)

하나씩 어떻게 달라지는지 알아보자.

📕 일반 함수 호출 (default binding)

기본적으로 this는 전역 객체를 가리킨다. Node 환경에서는 global 객체를, 브라우저 환경에서는 Window 객체를 가리킨다.

const func1 = function() {
  console.log(this);
}

func1(); // window 혹은 global 객체
// window.func1(); 또는 global.func1() 과 동일한 코드

function func2() {
  console.log(this);
}

func2(); // window 혹은 global 객체

위처럼 전역적으로 선언한 함수를 호출한 경우, Default binding으로 this에 전역 객체가 할당되게 된다.

const obj = {
  hello: function(){
    console.log(this);
  }
};

const func = obj.hello;

func(); // window 객체 출력

위처럼 객체의 메소드를 전역객체에 담은 뒤 호출한 경우에도 Default binding이 적용된다.

사실 Default Binding이라는게 어렵게 생각할 것 없이, 전역적으로 선언된 함수는 window 객체가 가지고 있다. 그러니 이를 호출하는 것은 window 객체가 호출하는 것과 같고, 그렇기에 this에 window 객체가 바인딩된다고 생각하면 될 것이다.

내부 함수에 대해서는 어떻게 작동할까?


function func1() {
  function func2() {
    console.log(this);
  }
  func2();
}

const obj = {
  func1: function(){
    function hello(){
      console.log(this);
    }
    hello();
  }
  func2: function(){
    setTimeout(function(){
      console.log(this);
    }, 100);
  }
};

func1(); // window 출력
obj.func1(); // window 출력
obj.func2(); // window 출력

마지막으로 위처럼 함수의 내부 함수 혹은 메소드의 내부 함수, 콜백함수인 경우도 this는 전역객체에 바인딩된다. 그런데 함수의 내부 함수는 window가 할당되는게 이해가 되더라도, 메소드의 내부 함수나 콜백 함수는 왜 window가 바인딩되는 것일까?

이것에 대해서 더글라스 크락포드는 “이것은 설계 단계의 결함으로 메소드가 내부함수를 사용하여 자신의 작업을 돕게 할 수 없다는 것을 의미한다” 라고 말했다고 한다. 말그대로 설계 단계의 결함이다. 이를 해결하려면 아래에서 얘기할 화살표 함수를 쓰거나 bind 등으로 명시적으로 지정해줄 수 있다.

strict mode(엄격 모드)

위에서 Default Binding으로 this에는 기본적으로 전역객체가 할당된다고 했다. 그러나 ES5에서 소개된 Strict mode를 사용하면, 전역 객체가 기본으로 바인딩 되지 않는다.

function strict() {
  'use strict`; // strict 모드 설정
  
  console.log(this);
}

strict();  // undefined 출력

window.strict();  // window 출력

위처럼 strict 모드를 설정하면, 기본으로 전역객체가 바인딩 되지 않기에 this가 undefined가 된다. 하지만, window 객체를 통해 호출을 하게 되면, window 객체가 바인딩되는데 이는 밑에서 설명할 암시적 바인딩의 맥락이다.

📒 객체의 메소드 호출 (암시적 바인딩)

객체의 메소드를 호출할 경우 암시적 바인딩이 적용되어, 해당 메소드를 호출한 객체에 바인딩된다.

function globFunc() {
  console.log(this);
}

const obj1 = {
  name: "철수",
  hello: function(){
    console.log(this.name);
  },
  glob: globFunc
};

const obj2 = {
  name: "영희"
};

obj2.hello = obj1.hello;

obj1.hello(); // 철수
obj2.hello(); // 영희

obj1.glob(); // obj1 

위처럼 객체의 메소드의 경우 this에 호출한 객체가 바인딩된다. 다른 객체, 심지어는 전역적으로 선언된 함수일지라도, 호출한 객체에 this가 바인딩되는 것을 볼 수 있다.

📗 call/apply/bind로 명시적인 바인딩

위처럼 암시적으로 this를 바인딩하는 것이 아니라 특정 함수에 정확히 원하는 객체를 바인딩해줄 수 있다.

call과 apply는 인자를 넘기는 방식만 다를 뿐, 동작자체는 같으므로 먼저 설명하겠다.

call & apply

call과 apply 메소드는 this를 특정 객체에 바인딩해주면서 함수를 호출해준다. 예시를 보면서 이해해보자.

function hello(message) {
  console.log(`안녕 나는 ${this.name}`);
  console.log(message);
}

const person1 = {
  name: "철수"
};

/*
	안녕 나는
    만나서 반가워
*/
hello("만나서 반가워");

/*
	안녕 나는 철수
    만나서 반가워
*/
hello.call(person1, "만나서 반가워!");
hello.apply(person1, ["만나서 반가워!"]);

call과 apply 모두 호출하는 함수의 this를 특정 객체로 바인딩해주면서 함수를 호출해준다. 그 과정에서 call은 인자들을 하나씩 넣어줘야하지만, apply는 배열 형태로 넣어주면 된다.

결과를 보면, hello(message) 함수는 전역으로 선언된 함수이니 그냥 호출을 하면, this에 전역 객체가 바인딩 된다. 그래서 그냥 hello(message)로 호출한 경우, 이름이 나오지 않는 것을 볼 수 있다.

그러나 명시적 바인딩을 통해 this를 person1으로 할당해주면, this.name에 값이 제대로 들어가는 것을 볼 수 있다.

bind

bind도 call/apply와 비슷하다. 그러나 bind는 이름에서 알 수 있듯이 this에 바인딩하는 역할을 위해 만들어졌다. 그래서 함수 호출을 하는 것이 아닌, this가 바인딩된 새로운 함수를 반환한다. 마찬가지로 예시를 보면서 알아보자.

function hello(message) {
  console.log(`안녕 나는 ${this.name}`);
  console.log(message);
}

const person1 = {
  name: "철수"
};


/*
	안녕 나는 철수
    만나서 반가워
*/
const bindedHello = hello.bind(person1, "만나서 반가워");
bindedHello();

hello.bind(person1, "만나서 반가워")();

위처럼 bind를 하면 this에 인자가 바인딩된 새로운 함수를 반환한다. 함수를 호출하는 것이 아닌 반환해주기 때문에, 콜백 함수를 등록할 때도 사용할 수 있다.

function hello(message) {
  console.log(`안녕 나는 ${this.name}`);
  console.log(message);
}

const person1 = {
  name: "철수"
};

setTimeout(hello.bind(person1, "만나서 반가워"), 100);

apply/call은 함수를 호출하기 때문에 콜백 함수를 등록하는 것은 불가능하지만, bind는 가능하다!

📘 생성자 함수 호출 (new 바인딩)

JS에서 생성자 함수는 객체를 생성하는 역할을 한다. 생성자 함수를 통해 만들어진 객체의 this에는 해당 인스턴스가 바인딩 된다.

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

const 철수 = new Person("철수");
console.log(철수); // Person {name: "철수"}

위처럼 new 연산자를 통해 함수를 호출함으로써, this를 인스턴스에 바인딩할 수 있다. 이게 어떻게 가능한 일인지는 아래 동작방식을 보면 알 수 있다.

생성자 함수의 동작 방식

  1. 빈 객체를 만든다.
  2. 1에서 만든 객체가 생성자 함수 내에서 사용될 this에 바인딩된다.
  3. 1에서 만든 객체의 Prototype를 생성자 함수의 Prototype가 가리키는 객체로 설정한다.
  4. 1에서 만든 객체에 this를 통해 생성한 프로퍼티와 메소드가 추가된다.
  5. 함수가 객체를 반환하지 않는 한 1에서 생성된 객체가 반환된다.
    • 만약 반환문이 this가 아닌 다른 객체를 명시적으로 반환하는 경우, this가 아닌 해당 객체가 반환된다.
    • 즉, 기껏 만들어둔 객체가 아닌 다른 객체가 반환되는 것이다.

위처럼 생성자 함수가 동작하는 과정에서 명시적으로 this를 바인딩해준다! 그 덕분에 생성자 함수를 통해 만든 객체에서는 this를 인스턴스로 사용할 수 있는 것이다.

😎 이해할 때쯤 치고 들어오는 화살표 함수

위에 작성한 예시들을 보면 화살표함수는 단 한개도 없다! 화살표 함수의 경우 this의 동작방식이 약간 다르기때문에 따로 공부할 필요가 있습니다.

function 키워드로 만드는 함수들의 경우 this가 실행 환경에 따라 동적으로 바인딩 됩니다. 그러나 화살표 함수로 만드는 함수는 this가 정적으로 정해지게 됩니다. 사실 화살표 함수는 자신의 this가 없습니다. 자신의 this를 갖는 대신, 화살표 함수를 둘러싸는 렉시컬 범위(Lexical Scope)의 this를 사용하기에, 바로 상위의 스코프의 this를 가리키게 됩니다.

이러한 특성 때문에, 콜백 함수로 화살표 함수를 넣어주게되면 따로 this를 바인딩하지 않아도 콜백함수를 호출한 메소드의 인스턴스가 this로 설정되게 됩니다. this를 신경쓸 필요가 없어지는 것이죠...

그런데 이러한 특징 때문에 화살표 함수를 사용하면 안되는 경우도 생깁니다.

🚨 화살표 함수를 사용하면 안되는 경우

객체의 메소드

객체의 메소드로 화살표 함수를 사용하면 어떻게 될까요? 화살표 함수는 자신의 this를 가지지 않으니, 해당 객체의 상위 컨텍스트를 가리키게 됩니다. 예시를 통해 알아봅시다.

const person = {
  name: '철수',
  hello: () => {console.log(`안녕 나는 ${this.name}`);}
}

// "안녕 나는" 출력
person.hello();

위 예시에서 hello 메소드 안에서 사용한 this는 전역객체인 윈도우 객체가 됩니다. 화살표 함수로 선언하였기에 해당 함수는 this가 없고 그 상위 컨텍스트를 참조하게 되는데, 그 상위 컨텍스트는 바로 전역객체인 window가 되겠죠. 그렇기 때문에 메소드에서 this를 사용할 것이라면, 화살표 함수를 사용하지 않는 것이 좋습니다.

prototype

const person = {
  name: '철수'
}

Object.prototype.hello = () => {console.log(`안녕 나는 ${this.name}`);}

// "안녕 나는" 출력
person.hello();

객체의 메소드와 같은 경우입니다. 화살표 함수 내부의 this는 인스턴스를 가리키지 않고, 그 상위 컨텍스트를 가리킵니다. 위 예시에서는 전역객체를 가리키죠.

생성자 함수

const Person = () => {};

const person = new Person(); // Uncaught TypeError: Person is not a constructor

화살표 함수는 this 이외에도 prototype 프로퍼티도 없습니다. 위에서 생성자 함수를 호출하면, prototype 프로퍼티도 업데이트 해주죠? 그래서 생성자 함수로 사용하려고 하면 에러가 발생됩니다.

addEventListener 함수의 콜백 함수

my_element.addEventListener('click', function (e) {
  console.log(this.className)           // my_element의 클래스명
  console.log(e.currentTarget === this) // true 
})

addEventListener의 콜백 함수의 this는 이벤트 핸들러의 대상 엘리먼트를 참조합니다. 즉, this == e.currentTarget입니다. 물론 이를 사용할 일이 없다면 화살표 함수를 사용하여도 되겠지만, 가능하다면 function 키워드로 작성하는 것이 버그를 줄일 겁니다.

📃 참고 링크

Function.prototype.bind() - JavaScript | MDN
Strict mode - JavaScript | MDN
this - JavaScript | MDN
함수 호출 방식에 의해 결정되는 this
자바스크립트 this 바인딩 우선순위
javascript this의 4가지 동작 방식
[JavaScript] 17. 생성자 함수에 의한 객체 생성
화살표 함수 - JavaScript | MDN
EventTarget.addEventListener() - Web API | MDN
화살표 함수에서 this

profile
날 어떻게 한줄로 소개해~

0개의 댓글