ABOUT ME

-

My Email
  • kdmin0211@gmail.com
  • Today
    -
    Yesterday
    -
    Total
    -

    • [Javascript] this?
      Javascript 2021. 9. 20. 23:37

      해당 글은 YOU DON'T KNOW JS(this와 객체 프로토타입, 비동기와 성능) 책을 읽으며 정리한 Post입니다.

      실제 책의 내용을 간략화 한 Post이고 실제로 책을 읽으면 많은 예제와 자세한 내용이 서술되어 있습니다:D

      필자는 new Object, Object.create(), literal notation를 통해 객체를 사용하며 this를 많이 사용했습니다. 단순히 this를 현재 위치한 객체의 스코프를 가리키는 것이라고만 알고 사용하였고 생각대로 출력이 안 되는 경우 arrow function을 사용하여 물이 새는 부분만 급하게 막으며 지나갔습니다. 필자가 JS에 대해서 잘 설명한다고 생각하는 YOU DON'T KNOW JS를 읽으며 한번 제대로 이해하고자 포스트를 작성했습니다.

       

      this?

      책의 처음에서 this 란 마법이라고 칭하고 있습니다. 그 후 this에 대해서 잘못 해석한 의미에 대해 2가지를 서술하는데 모두 필자가 한 번씩 생각했었던 this의 의미였습니다..

       

      자기 자신

      필자가 처음 this를 접하고 생각하던 의미입니다. 당연히 this라는 단어의 어원 자체로 단순히 this가 작성된 현재 즉, 자신이라 생각했던 것입니다. JS의 타입들을 공부하며 함수도 객체라는 사실을 알 수 있었고 함수 안에서 this를 사용했으나 뜻대로 되진 않았습니다.

      부끄럽지만 필자가 이해했던 간단한 예제를 들어보겠습니다.

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

      해당 코드에서 this 자체는 func1 객체를 가리키며 func1.number, this.number 는 같은 값을 가질 것이라고 생각했습니다.

      당연히 func1.number = undefined, this.number = 1 이 출력됩니다. 그 이유는 this 가 어떻게 호출되었는가에 따라 결정되며 현재 예제에서 this는 글로벌 스코프 window 가 될 것입니다. 실제로 func1 내부에서 console.log(this)는 window를 출력합니다.

      그 반면에 func1.number 는 랙시컬 스코프를 통해 func1 함수 객체를 참조하게 되며 내부 number를 접근했으나 선언된 적이 없으니 undefined 가 출력된 것입니다. 자세한 설명은 뒤에 서술하겠습니다.

       

      자신의 스코프

      이 의미 역시 필자가 this 가 자기 자신이 아니라는 것을 알게 된 후 책을 읽기 전까지 생각했던 의미입니다.

      필자는 그동안 이해하고 사용하던 의미가 아니라는 것에 당황했고 아닌 이유에 대해서 바로 이해하기는 힘들었습니다. 이때 책에서 강조하는 것은 this는 함수의 랙시컬 스코프를 참조하지 않는 것입니다.

       

      그럼 this는 도대체 무엇인가

      오해를 불러오는 큰 이유는 this 가 결정되는 시점입니다. this는 코드의 작성 시점이 아니라 런타임 시점에 바인딩되며 이것을 this 바인딩이라고 합니다. 오로지 어떻게 호출되어 사용되었는가 즉, 함수의 호출부가 this 가 무엇을 참조할지 결정합니다.

      이제 호출부가 this를 어떻게 바인딩하는지 4가지 규칙을 알아보겠습니다. 

      기본 바인딩

      평범한 함수의 호출의 경우로 단독 함수 실행 규칙으로 this 바인딩은 전역 객체 window가 됩니다. 나머지 규칙이 유효하지 않은 경우 적용되는 기본 규칙입니다.

      function f1() {
          console.log('function f1 a: ', this.a);
          f2();
      }
      
      function f2() {
          console.log('function f2 a: ', this.a);
          f3();
      }
      
      function f3() {
          console.log('function f3 a: ', this.a);
      	debugger;
      };
      
      var a = 3;
      
      f1();

      글로벌 스코프에서 var a = 3으로 전역 객체에 a 프로퍼티를 생성했습니다. (이때 const, let의 경우 전역 객체의 프로퍼티로 생성되지 않습니다.)

      그 후 f1, f2, f3 함수는 전형적인 함수의 호출 방식으로 호출되었으며 this는 window 가 됩니다.

      실제 실행 결과입니다.

       

      암시적 바인딩

      함수의 호출부에 객체의 소유/포함 여부를 확인합니다. 위에서 사용한 예제를 약간 수정하겠습니다.

      function f1() {
          console.log('function f1 a: ', this.a);
          f2();
      }
      
      function f2() {
          console.log('function f2 a: ', this.a);
          f3();
      }
      
      function f3() {
          console.log('function f3 a: ', this.a);
      	debugger;
      };
      
      var a = 3;
      
      var obj = {
      	a: 30,
      	f1,
      }
      
      obj.f1();

      여기서 각 함수의 this.a 가 무엇을 출력할지 예상해봅시다.

      필자는 호출부에서 객체의 소유/포함 여부를 생각하며 f1 함수의 this.a 는 30 이 출력될 것은 예상했으나 f2, f3 함수의 this.a 는 확답을 내지 못했습니다.

      실제 결과입니다.

      결과를 보며 당연한 결과라는 생각이 들었습니다. f1의 호출부는 obj 객체로 f1를 참조하고 있습니다. 이경우 obj 객체는 f1 호출 시 함수의 레퍼런스를 소유/포함으로 볼 수 있으며 30을 출력합니다.

      그러나 f2, f3의 호출부를 확인하면 일반적인 함수의 호출입니다. 그러므로 f2, f3에서의 this는 기본 바인딩으로 인하여 window 가 되며 3을 출력합니다.

      여기서 예시를 좀 더 변경해보겠습니다.

      function f1() {
          console.log('function f1 a: ', this.a);
          this.f2();
      }
      
      function f2() {
          console.log('function f2 a: ', this.a);
          this.f3();
      }
      
      function f3() {
          console.log('function f3 a: ', this.a);
      	debugger;
      };
      
      var a = 3;
      
      var obj = {
      	a: 30,
      	f1,
      	f2,
      	f3
      }
      
      obj.f1();

      f2, f3 함수 역시 obj에 프로퍼티로 참조하도록 변경했습니다. 추가로 f2, f3 함수의 호출부를 this.f2(), this.f3()로 변경했습니다. 변경 내용을 실행해보겠습니다.

      obj.f1()으로 실행된 f1의 this는 암시적 바인딩 규칙으로 obj로 바인딩됩니다. f1 내부의 this.f2() 역시 obj의 f2 가 될 것입니다. 즉, obj.f2() 호출부와 동일하므로 이 역시 호출부에서 obj 객체가 f2 함수의 레퍼런스를 소유/포함하므로 f2 함수에서 this 역시 obj 가 될 것이므로 30 이 출력됐습니다. f3 역시 마찬가지입니다.

       

      암시적 소실

      책에서 this 바인딩의 이해를 방해하는 요소 중 하나로 서술됩니다. 필자 역시 실제로 겪었던 내용으로 드디어 이유를 알 수 있었습니다.

      위의 예제를 간소화하여 재사용하겠습니다.

      function f1() {
          console.log('function f1 a: ', this.a);
          debugger;
      }
      
      var a = 3;
      
      var obj = {
          a: 30,
          f1,
      }
      
      var func = obj.f1;
      func();

      이경우 testFunc()의 this.a 값은 obj 객체가 f1를 소유/포함하는 호출부로 생각하여 30이 출력될 것을 예상했습니다. 하지만 실제 출력 값은 3이 나옵니다.

      그 이유는 생각보다 간단했습니다.

      var testFunc = obj.f1 는 obj의 f1를 참조하는 것이 아닌 f1 함수를 직접 가리키는 레퍼런스를 가리킵니다. 즉, testFunc() 호출부는 일반적인 함수 호출이 되어 기본 바인딩이 적용되어 this 가 window 객체로 결정되어 3이 출력됩니다.

       

      이런 암시적 소실은 콜백 함수에서 많이 만날 수 있습니다.

      function f1() {
          console.log('function f1 a: ', this.a);
          debugger;
      }
      
      var a = 3;
      
      var obj = {
          a: 30,
          f1,
      }
      
      setTimeout(obj.f1, 100);

      Web API의 setTimeout으로 예를 들겠습니다. 위와 유사한 형식으로 call back 함수로 원하는 작업을 인자로 넘기는 경우가 많습니다. 하지만 setTimeout 콜백의 출력은 3이 나올 것입니다.

      암시적 손실을 생각하면 쉽게 이해할 수 있습니다. (필자는 이 부분에서 암시적 손실이 정확이 왜 발생하는지 모르는 상태에서 지속적으로 arrow function이나 다른 방식으로 코드를 작성해왔습니다..)

      function setTimeout(callBack, delay) {
      	//delay
      	callBack();
      }

      setTimeout 역시 위 코드와 같은 기능을 수행하는 내장 함수입니다. 여기서 callBack 은 obj.f1 에서 f1 함수를 직접 가리키는 레퍼런스입니다. 즉, callBack() 호출부는 기본 바인딩으로 this 가 window로 바인딩되며 3을 출력한 것입니다. (그동안 왜 필자의 뜻대로 코드가 돌아가지 않은지 확실히 알 수 있었습니다.)

       

      명시적 바인딩

      앞서 서술한 암시적 바인딩은 오로지 객체의 프로퍼티로 함수 레퍼런스를 호출하는 방식으로 암시적으로 this 를 바인딩했습니다. 이와 다르게 명시적으로 this 를 바인딩하는 규칙이 명시적 바인딩입니다.

      call(), apply() 함수가 명시적 바인딩에 사용됩니다. call(object, arg1, arg2, ...), apply(object, [arg1, arg2,...]) 의 형태로 사용되며 호출부에서 this는 인자인 object로 바인딩됩니다.

      function f1(arg) {
          console.log(this.name);
          console.log(arg);
      }
      
      var obj = {
          name:'this 는 obj'
      };
      
      f1.call(obj, 'argument')

      이처럼 f1 의 this는 인자인 obj 가 되었습니다.

       

      하드 바인딩

      명시적 바인딩을 활용한 형태로 소개되었습니다. 하드 바인딩은 함수를 감싸는 형태로 명시적 바인딩을 수행합니다.

      function f1(args) {
          console.log(this.name);
          console.log(args);
      }
      
      var obj = {
          name: 'this 는 obj',
      }
      
      var hardBinding = function(args) {
          f1.apply(obj, args);
      }
      
      hardBinding(['argument']);

      간단한 예시입니다. hardBinding 함수 표현식 내부에 명시적 바인딩을 수행하는 f1.apply 호출부가 존재합니다. 이경우 hardBinding을 통해 호출되는 f1의 this는 항상 obj입니다. 실제로 ES5 내장 유틸리티 Function.prototype.bind 역시 하드 바인딩 형태로 구현되어 있다고 합니다.

       

      new 바인딩

      마지막으로 인스턴스의 생성의 역할인 new 연산자입니다. new 연산자를 사용하여 생성자를 호출시 새로운 객체를 만들며 함수 내부는 this가 새 객체에 바인딩됩니다.

      function f1(name) {
          this.name = name;
      }
      
      var instance1 = new f1('new binding');
      
      console.log(instance1.name);

       

       

      우선순위

      모든 바인딩 규칙들이 같은 우선순위를 가지며 같이 실행되면 정말 마법 같은 일이 발생하며 this를 예상할 수 없을 것입니다. 다행히? 바인딩에는 아래와 같은 적용 순서가 있습니다. 실제 바인딩 규칙 우선순위에 대해서는 책에 자세히 서술되어 있습니다!

      1. new 생성자를 통해 함수의 호출부 -> new 바인딩이 적용됩니다.

      2. bind, soft bind, call, apply 를 사용한 함수의 호출부 -> 명시적 바인딩이 적용됩니다.

      3. 객체를 소유/포함하는 함수의 호출부 -> 암시적 바인딩이 적용됩니다.

      4. 마지막으로 1, 2, 3이 적용되지 않은 경우 기본 바인딩이 적용됩니다.

       

      이제 this 를 이해하고 예측하며 사용할 수 있게 되었습니다. :)

      'Javascript' 카테고리의 다른 글

      [Javascript] Scope  (1) 2021.09.14

      댓글

    Designed by Tistory.