6 Apr 2019

ECMAScript6) What is 'This'?

What is ‘This’?

이거뭐야

this는 위 그림에서 보이듯이 execute context의 this를 말한다.

Javascript의 Excution context 내부를 살펴보면 this라는 Value의 자리가 존재한다. 흔히 타 언어에서 사용하는 selfthis객체, 자기자신 을 의미하기 때문에 이해하기가 수월하였다. 하지만 JS의 this는 개념이 조금 다르기 때문에 이해하는 것이 쉽지않다.

뭐 그래도 타 언어와 동일하게 this는 객체를 참조하는 것이니만큼 일단 선행되어야할 기본지식은 Object - Prototype 에 대한 이해이다.

그럼 먼저 Object - Prototype 에 대해 짚고 넘어가자.


1) Prototype

자바스크립트는 prototype 기반의 객체지향적 언어

일반적인 객체지향언어는 class기반이지만, 자바스크립트는 기본적으로 class 가 없다. (ES6부터 문법만 추가)

때문에 상속도 없다. -> (bind를 통해 상속을 구현할 수는 있다.)

이러한 특성을 prototype을 통해 구현해낸다.

  • 자바스크립트는 function()new 연산자를 통해 클래스를 비슷하게 흉내낼 수 있음.

      function Person() {
          this.eyes = 2;
          this.nose = 1;
      }
        
      var kim  = new Person();
      var park = new Person();
    
      console.log(kim.eyes);  // => 2
      console.log(kim.nose);  // => 1
      console.log(park.eyes); // => 2
      console.log(park.nose); // => 1
    
  • kimparkeyes, nose 2개의 프로퍼티를 가지고 있음. 객체를 100개 만들면 200개 변수 메모리가 할당됨.

  • prototype은 이러한 비효율성을 해결해 줌.

      // 클래스 선언
      function Person() {}
      Person.prototype.eyes = 2;
      Person.prototype.nose = 1;
    
      var kim = new Person();
      var park = new Person();
    
      console.log(kim.eyes);  // -> 2
      console.log(park.eyes);  // -> 2
    

이제부터 생성자로서 사용될 함수를 선언하였을 때, 해당 함수를 통해 객체를 생성하였을 때 일어나는 일들을 살펴볼 것이다.

  • 자바스크립트에는 __proto__를 통한 prototype Linkprototype property가 가리키는 prototype Object 가 존재한다. 이 둘을 통칭하여 prototype이라고 한다.
      function A() {}; // 빈 객체를 생성하는 함수 선언
    
      var A = new A(); // new 연산자를 통한 객체 생성 
      console.log(A); 
    

이제부터 저 위 그림을 파헤쳐보자!

  • 객체는 언제나 함수에서 시작된다! 따라서 다음 과정도 함수로부터 시작되는 것이다.(literal을 통한 객체 생성.)

      var obj = {};
    
  • 이는 아래와 같은 코드이다.
      var obj = new Object();
    
  • 즉, Object() 는 자바스크립트에서 기본적으로 제공하는 함수 이다.

  • 또한 이 함수를 통해 생성되는 prototype object가 최상위 객체를 의미하며, __proto__의 종착점이다.

1-1. Object 의 실체

  • 함수가 정의되는 순간 발생하는 일 2가지는 다음과 같다.

    생성자로 사용될 함수 뿐 만 아니라 모든 함수가 이러한 과정을 거치며, 이를 이해함으로써 prototype에 대한 이해가 가능할 것이다.

1. Constructor(생성자) 자격 부여

  • Constructor 자격이 부여되면 new 연산자를 통해 인스턴스를 생성할 수 있게 됨.

2. prototype Object의 생성

  • 함수 뿐만 아니라 prototype Object 도 함께 생성된다.

  • 위 그림과 동일하게 함수는 arguments, name, caller, [[scope]] 등과 함께 function Object(왼쪽)의 prototype 프로퍼티를 통해 prototype Object 을 참조할 수 있다. prototype Object는 일반적인 객체와 같고, 기본적인 프로퍼티로 constructor__proto__를 가지고 있다.

  • 함수 객체만 유일하게 prototype property를 가지며, 이 property는 해당 함수로 새롭게 생성되는 객체가 가질 과 같다.

  • 함수 객체가 생성하는 prototype Obect만이 오로지 constructor라는 property를 가지며, 생성자로 사용된 function Object를 가리킨다.

    • __proto__는 prototype Link 이다. (아래에서 설명)

    • 다음 코드를 보고 다시 이해해보도록 하자.

        function Person() {};
        Person.prototype.eyes = 2;
        Person.prototype.nose = 1;
      
        var kim = Person();
        var park = Person();
      
    • 이제 왜 Person.prototype을 사용하는지 알 수 있을 것이다.

    • Person함수가 생성되며 함께 생성된 prototype Objecteyesnose 프로퍼티를 추가하여 Person() 함수로 생성된 객체는 prototype Objecteyes, nose 프로퍼티를 참조할 수 있다.

  • prototype Link ?

    • 위의 예제에서 kimeyes 프로퍼티에 접근 했을 때, kim에게는 eyes라는 프로퍼티가 없는데도, 2라는 값을 반환하는 것을 볼 수 있다.

1-2. __proto__ property

  • 생성자의 prototype Obect의 property가 kim이라는 객체의 property가 되는 것이 아닌 것을 볼 수 있다.

  • 그럼에도 불구하고 가능한 것은 kim__proto__ 프로퍼티에 의해서 가능하다.

    **__proto__** 는 인스턴스가 생성될 때 조상이었던 함수의 prototype Object 를 가리킨다.

  • __proto__ 의 형태를 보자.

2) Prototype Chain

객체가 생성될 때 가지는 __proto__ property에 의해 단계적으로 상위 생성자 함수의 prototype Obect의 property와 method가 참조 가능하게 하는 reference chain을 의미한다.

  • Scope Chain과 유사한 형태.

Scope Chain이라는 array로 구현되어 있다면, Prototype Chain은 말그대로 reference(참조)의 연속으로 구현 되어있다.

함수와 객체(인스턴스), prototype Object의 관계

함수 또한 하나의 객체임을 기억하며, 간단한 예제로 지금까지 공부했던 것 살펴보자.

  • 요약하자면, function Person() {} 를 생성하면서, Person prototype Object 가 함께 생성되고, 거기에는 constructor, __proto__ 라는 프로퍼티가 존재한다.

  • Person() 을 통해 Person의 인스턴스 kim을 생성했을 때, 그 kim 에게는 __proto__ 프로퍼티가 기본적으로 존재하고, 이는 Person prototype Object의 참조를 담고있다.

  • kim 인스턴스가 eyesnose 프로퍼티를 직접 가지고 있지 않기 때문에 eyes, nose 프로퍼티를 찾을 때 까지 상위 프로토타입을 탐색하고, 최상위 프로토타입인 Object의 Prototype Object 까지 탐색했는데도 못찾은 경우 undefined를 리턴한다.

  • 이렇게 __proto__를 통해 상위 프로토타입과 연결되어 있는 형태를 프로토타입 체인(prototype chain) 이라고 한다.

이제 하나의 예제를 더 살펴보면서 옵져버 패턴의 의미를 살펴보자.

var A = function () { };
A.prototype.x = function () {
     console.log('hello');
};
var B = new A();
var C = new A();

B.x();
> hello 

C.x();
> hello 

A.prototype.x = function () {
     console.log('world');
};

B.x();
> world

C.x();
> world

prototype chain을 통해 상속되는 x method는 각 객체마다 할당되는 것이 아니라 Prototype Object의 method를 Prototype Chain을 통해 참조하는 것이다.
즉, method를 공유하게 되는데, 위의 예제처럼 객체간의 관계가 1:N의 형태로 정의되는 것, 상태를 가지고 있는 객체와 상태의 변경을 관리하는 객체가 구분되어있는 객체 간의 관계를 Observer pattern라고한다.

  • 즉, 하나의 객체를 관리함에 따라 다른 객체의 변화나 영향을 줄 수 있다는 것을 정의해둔 것이다.

옵저버 패턴 말고도 Javascipt에서는 다양한 패턴이 존재한다.

3) this

지금까지 Prototype 기반의 객체지향적 언어인 Javscript에 대해 공부해보았다. 객체의 생성과 호출에 대해 이해가 되었다면 이제 본격적으로 this에 대해 알아보자.

this에 바인딩되는 객체는 객체의 생성시점과 호출방식 에 따라 달라진다.

1. 기본적으로 전역객체에 바인딩 되는데, Browser-side는 window에, Server-side는 global에 바인딩된다.

  • chrome의 console

  • nodejs의 console

2. 함수의 내부함수일 때에도, 심지어 method의 내부함수일 때에도 this는 전역객체에 바인딩된다.

  • 함수의 내부함수일 때,

      function foo() {
      console.log("foo's this: ",  this);  // window
      function bar() {
          console.log("bar's this: ", this); // window
      }
      bar();
      }
      foo();
    
  • method의 내부함수일 때,

    method 또한 하나의 함수이므로 위의 예제와 동일한 가정을 의미한다.

    
      var value = 1;
    
      var obj = {
      value: 100,
      foo: function() {
          console.log("foo's this: ",  this);  // obj
          console.log("foo's this.value: ",  this.value); // 100
          function bar() {
          console.log("bar's this: ",  this); // window
          console.log("bar's this.value: ", this.value); // 1
          }
          bar();
      }
      };
    
      obj.foo();
    

3. 객체의 method로 함수가 호출될 때에는, 호출하는 객체에 바인딩된다.

var obj1 = {
name: 'Lee',
sayName: function() {
    console.log(this.name);
}
}

var obj2 = {
name: 'Kim'
}

obj2.sayName = obj1.sayName;

obj1.sayName(); //Lee
obj2.sayName(); //Kim

4. 새롭게 생성된 객체가 Prototype Object의 method를 호출할 때에도 호출한 객체에 바인딩되며, Prototype Object 또한 하나의 객체로 바인딩 될 수 있다.

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

Person.prototype.getName = function() {
return this.name;
}

var me = new Person('Lee');
console.log(me.getName());

//prototype 또한 하나의 객체를 의미한다.

Person.prototype.name = 'Kim';
console.log(Person.prototype.getName());

5. new 연산자를 통한 생성자 함수 호출

  • 생성자 함수라고 해서 다른 문법이 존재하지 않는다.

    이러한 의미는 생성자로서의 함수가 아니더라도 new 연산자를 사용할 수 있다는 뜻이다.

  • new 연산자를 사용하지 않으면, 객체를 생성하지 않으므로 의미가 없다.

    이 것은 new 연산자가 호출 되었을 때의 과정을 살펴보면서 이해할 수 있다.

  • 예제를 보면서 이해하자.

     function Person(name) {
         // 생성자 함수 코드 실행 전 -------- 1
         this.name = name;  // --------- 2
         // 생성된 함수 반환 -------------- 3
     }
    
     var me = new Person('Lee');
     console.log(me.name);
    
    1. 빈 객체 생성 및 this 바인딩

      new 연산자에 의해 Object prototype object의 빈 객체가 생성되고, 현재 execute contextthis에 이 빈 객체를 바인딩한다.

    2. Prototype Chain 연결

      이 후, 빈 객체의 __proto__를 생성자 함수의 prototype object에 link시켜 chain을 완성시킨다.

    3. this를 통한 property / method 생성.

      바인딩되어 있는 빈 객체에 생성자 함수에 명시된 property와 method를 추가시킨다.

    4. 새롭게 생성된 객체를 return

  • 생성자 함수를 new를 사용하지 않고 호출한다면?

    • 위에서 보다시피 함수 내부에서도 전역객체에 할당된다고 하였다. new를 사용하지 않았을 때 일반적인 함수와 동일하게 동작함을 볼 수 있다.

        function Person(name) {
              
        this.name = name;
        };
      
        var me = Person('Lee');
      
        console.log(me); // undefined
        console.log(window.name); // Lee
      

6. 고차함수를 이용한 호출

이 후의 Chapter에서 여러가지 고차함수를 다뤄볼 예정이나, 일단 함수의 호출에 사용되는 고차함수들을 먼저 살펴보자.

1) apply() 함수를 이용한 호출.
func.apply(thisArg, [argsArray])

func - apply 함수를 호출할 함수 객체명.
thisArg - 해당함수 호출시, this에 바인딩할 객체명.
argsArray -해당함수 호출시, 필요한 인자값.

  • 함수를 호출하되, this에 바인딩되는 객체를 임의로 지정해주는 것이다.
  • 주요기능이 함수를 호출하는 것에 있다.

  • Function.prototype.call()

    함수객체의 최상위 계층의 Function.prototype의 method이므로 모든 함수객체가 가지고 있는 method이다.

    예제로 살펴보자.

      function hello(says){
          this.name = 'chanwoo';
          this.sayHello = function(){
              console.log(this.name + says); 
          }
      }
    
      var sub = {
          name : 'sub'
      }
    
      hello.apply(sub, [' hi']);
      sub.sayHello();
    
      // output
      // chanwoohi
    

    apply함수를 통하여 hello라는 생성자 함수를 호출하되, new를 사용하지 않고 기존 객체에 this를 바인딩시켜 동작하게하는 모습이다.

    • callback 함수의 this를 사용하기 위해서 단순 함수 호출이 아닌, 객체까지 binding하여 호출한다.
      function Person(name) {
          this.name = name;
          }
    
          Person.prototype.doSomething = function(callback) {
          if(typeof callback == 'function') {
              // --------- 1
              callback();
          }
      };
    
      function foo() {
      console.log(this.name); // --------- 2
      }
    
      var p = new Person('Lee');
      p.doSomething(foo);  // undefined
    

    이전에도 설명하였듯, 객체의 메소드이더라도 내부함수의 경우, this가 전역객체에 바인딩된다고 이야기하였다. callback함수에도 마찬가지이므로 일반호출이 아닌, 현재의 객체까지 call로 넘겨주는 것이다.

      function Person(name) {
          this.name = name;
          }
    
          Person.prototype.doSomething = function (callback) {
          if (typeof callback == 'function') {
              callback.call(this);
          }
      };
    
      function foo() {
      console.log(this.name);
      }
    
      var p = new Person('Lee');
      p.doSomething(foo);  // 'Lee'
    
2) call() 함수를 이용한 호출.

apply와 동일하지만, arguments들을 각각으로 넣어주는 경우 사용한다.

3) bind() 함수를 이용한 함수객체 바인딩

기존 new 생성자는 빈 객체를 생성하여 __proto__를 연결하는 방식이나 빈 객체를 생성하는 것이 아닌, 기존 객체에 바인딩하여 method, property를 정의하는 방식이 될 것.

예제 추가예정

4) 결론 및 정리

JS에서 this객체, 자기 자신 이다. 여기까지만 본다면 다른 언어의 this와 동일하지만 ObjectPrototype 의 개념이 들어가면서 JS만의 특별한 this가 완성되었다. 콜백, 이벤트등 this에 바인딩되는 객체와 시점이 애매모호할 때 매우 필요한 학습이라고 생각하기에 작성하게 되었다.

  • 더 필요한 학습

    1. Observer pattern
    2. 고차함수
    3. Webpack

Tags: