JS Class에 관하여

FGPRJS·2021년 11월 2일
1

JS에서는 class 키워드가 존재하지만, 그것은 C#이나 Java등의 객체지향프로그래밍을 기반으로 하는 언어의 class패턴을 어떻게든 흉내낸것에 불과하며 실제 class패턴의 개념과는 궤를 달리한다.(JS의 객체는 프로토타입 기반의 객체라고 한다.)

그래서 Interface도 없고 C++같은 다중상속 방식을 사용할 수도 없으며 (단, mixin이나 Proxy Object등을 사용하여 비슷하게 구현할 수 있다고 한다.관련 링크)
virtual(부모에서 선언되어있어 그대로 쓸수도, 오버라이드 할 수도 있음)도,
abstract(부모는 있다고만 하고 구현은 자식들이)
private(외부에서 찾을 수 없음)
public(누구나 접근 가능)
protected(관련자(자식)만 접근 가능)등의 선언도 무언가 모호하거나 아예 없다. (TypeScript에는 비슷하게 구현할 수 있다고 한다.)

그러면 JS의 class키워드는 실제로 무슨 작업을 하는지에 대하여 유추해본다.


다음은 class를 사용하는 예시이다.

class OBJ{
    constructor(){
        this.i =  10;
        this.arrow =  () => console.log(this.i, this);
        this.expression = function() {
            console.log( this.i, this);
        };
    }
};

let obj = new OBJ();

obj.arrow();
obj.expression();

다음 코드는 NodeJSBabel 트랜스파일러를 통하여 모던하지 않은 JS만 읽을 수 있는 환경에서도 코드가 구동될 수 있게 바꾸어준 코드중의 일부이다. (실제로는 더 많은 작업을 해놓는다.)
이것이 완전히 JSclass키워드가 하는 일을 대변할 수는 없겠지만, 최소한 Babelclass를 어떻게 생각하는지, 그리고 그것을 받은 파이어폭스 브라우저가 어떻게 생각하는지는 알 수 있을 것이다.

function _classCallCheck(instance, Constructor) {
    if (!(instance instanceof Constructor)) { 
      throw new TypeError("Cannot call a class as a function"); 
    } 
  }
  
  var OBJ = function OBJ() {
      var _this = this;
  
      _classCallCheck(this, OBJ);
  
      this.i = 10;
      this.arrow = function () {
          return console.log(_this.i, _this);
      };
      this.expression = function () {
          console.log(this.i, this);
      };
  };
  
  ;
  
  var obj = new OBJ();
  
  obj.arrow();
  obj.expression();

기본적으로, class라는 키워드 자체가 없다.
class로 선언했던 OBJ는 어떤 함수이다.

var OBJ = function OBJ() {
	...
}

이 함수는 new연산자와 함께 호출된다. new연산자도 다른 언어들이 수행하는 new와 하는 행동(추상화되어있는 class를 인스턴스화 하여 메모리에 할당하는 과정)이 완전히 다르다.

var obj = new OBJ();

JS에서 함수에서만 사용할 수 있는 new 키워드를 사용한 new OBJ()가 하는 행동은 다음과 같다.
new 연산자

  1. OBJ.prototype을 상속하는 새로운 객체가 하나 생성된다.
    • 현재 OBJ의 prototype은 window/global이다.
  2. 명시된 인자, 그리고 새롭게 생성된 객체에 bindthis와 함께 생성자 함수(constructor) OBJ가 호출된다.
    • this키워드를 bind한다.
  3. 생성자 함수에 의해 리턴된 객체는 전체 new 호출 결과가 된다. 만약 생성자 함수가 명시적으로 객체를 리턴하지 않는 경우, 첫 번째 단계에서 생성된 객체가 대신 사용된다. (일반적으로 생성자는 값을 리턴하지 않는다. 그러나 일반적인 객체 생성을 재정의(override)하기 원한다면, 그렇게 하도록 선택할 수 있다.

함수 안의 내용은 다음과 같다.

  var OBJ = function OBJ() {
      var _this = this;
  
      _classCallCheck(this, OBJ);
  
      this.i = 10;
      this.arrow = function () {
          return console.log(_this.i, _this);
      };
      this.expression = function () {
          console.log(this.i, this);
      };
  };

_this라는 변수를 통해 this에 접근할 수 있게 된다.
_classCallCheck를 수행한다. 이 함수는 다음과 같다.

function _classCallCheck(instance, Constructor) {
    if (!(instance instanceof Constructor)) { 
      throw new TypeError("Cannot call a class as a function"); 
    } 
  }

인자로 받은 instanceConstructorinstanceof연산이 false인 경우 타입에러를 발생시키는 함수이다.
즉, Constructorinstance프로토타입 체인 중에 찾을 수 없는 것이라면 오류를 발생시킨다.
여기서는 OBJthis프로토타입 체인속한다면, true이다.

즉, class 키워드가 기본적으로 수행하는 것은 해당 class 키워드를 함수로 취급하고, new로 해당 함수를 호출하되, 호출한 함수의 prototype과 같은 prototype을 갖는 새로운 객체를 만들고, 그것을 thisbind하면서 호출한다. 따라서 함수 안의 모든 this는 다른 어떤것을 return하지 않는 이상 그 새로운 객체가 되며, 그것을 return하는 것이다.

번거로우면서도 이게 전부가 아니라는 것으로 완벽한 분석은 힘들지만, 최소한 기존 객체지향프로그래밍의 class패턴과는 완전히 다르다는것정도는 알 수 있다.

기타사항으로 특이한 점을 하나 발견할 수 있는데,

this.arrow = function () {
  return console.log(_this.i, _this);
};
this.expression = function () {
  console.log(this.i, this);
};

arrow화살표함수였고, expression익명함수였다. 내용은 비슷했다.
하지만 arrow에서 표시하던 this는 이전에 만든 자신을 지칭하는 _this가 되어있고, expression에서 표시하던 this는 말 그대로 this가 되어있다.
이 경우 실제로 출력하는 차이가 없어보인다. 사실 이전의 _this 변수가 이 화살표 함수때문에 생긴 것이다. 관련 정보 링크

객체는 extends 키워드를 통하여 부모 class를 상속받을 수 있는 것처럼 보이게 할 수 있다.

class Parent{
    constructor(){

    }

    doParentAction(){
        console.log("DO PARENT");
    }

    doOverrideAction(){
        console.log("THIS IS PARENT FUNCTION");
    }
}

class Child extends Parent{
    constructor(){
        super();
    }

    doChildAction(){
        console.log("DO CHILD");
    }

    doOverrideAction(){
        console.log("THIS IS CHILD FUNCTION");
    }
}

let child1 = new Child();

child1.doChildAction(); // DO CHILD
child1.doOverrideAction(); // THIS IS CHILD FUNCTION

이 함수를 NodeJS의 development환경에서 Babel 트랜스파일러를 통하여 변환하고, 파이어폭스 브라우저를 통하여 감지된 소스의 일부이다. 다음은 실제로 동일하게 동작시킬 수 있는 최소한의 코드이며 실제로는 이것보다 많은 작업을 수행한다.

var _createClass = function () {
  function defineProperties(target, props) {
    for (var i = 0; i < props.length; i++) {
      var descriptor = props[i];
      descriptor.enumerable = descriptor.enumerable || false; 
      descriptor.configurable = true; 
      if ("value" in descriptor)
        descriptor.writable = true;
      Object.defineProperty(target, descriptor.key, descriptor);
    } 
  } return function (Constructor, protoProps, staticProps) {
    if (protoProps)
      defineProperties(Constructor.prototype, protoProps); 
    if (staticProps) 
      defineProperties(Constructor, staticProps); return Constructor; }; }();

function _possibleConstructorReturn(self, call) {
  if (!self) { 
    throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); 
  } 
  return call && (typeof call === "object" || typeof call === "function") ? call : self; 
}

function _inherits(subClass, superClass) { 
  if (typeof superClass !== "function" && superClass !== null) { 
    throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); 
  } 
  subClass.prototype = Object.create(superClass && superClass.prototype, 
  { 
    constructor: 
    {
      value: subClass, 
      enumerable: false, 
      writable: true, 
      configurable: true 
    } 
  }
  ); 
  
  if (superClass) 
    Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; 
}

function _classCallCheck(instance, Constructor) {
  if (!(instance instanceof Constructor)) {
    throw new TypeError("Cannot call a class as a function"); 
  } 
}


var Parent = function () {
    function Parent() {
        _classCallCheck(this, Parent);
    }

    _createClass(Parent, [{
        key: "doParentAction",
        value: function doParentAction() {
            console.log("DO PARENT");
        }
    }, {
        key: "doOverrideAction",
        value: function doOverrideAction() {
            console.log("THIS IS PARENT FUNCTION");
        }
    }]);

    return Parent;
}();

var Child = function (_Parent) {
    _inherits(Child, _Parent);

    function Child() {
        _classCallCheck(this, Child);

        return _possibleConstructorReturn(this, (Child.__proto__ || Object.getPrototypeOf(Child)).call(this));
    }

    _createClass(Child, [{
        key: "doChildAction",
        value: function doChildAction() {
            console.log("DO CHILD");
        }
    }, {
        key: "doOverrideAction",
        value: function doOverrideAction() {
            console.log("THIS IS CHILD FUNCTION");
        }
    }]);

    return Child;
}(Parent);

var child1 = new Child();

child1.doChildAction(); // DO CHILD
child1.doOverrideAction(); // THIS IS CHILD FUNCTION
profile
FGPRJS

0개의 댓글