[코어 자바스크립트]7. 클래스

Donghun Seol·2022년 11월 30일
0

코어자바스크립트

목록 보기
7/7
post-thumbnail

클래스

자바스크립트에서 class는 ES6에서 최초로 추가된 문법이다. class도 내부적으로 일부 프로토타입을 활용하고 있으므로 ES5에서 프로토타입을 활용해 클래스를 흉내내는 방법을 알고 넘어갈 필요가 있다.

클래스와 인스턴스의 개념

일반적으로 클래스는 블루프린트, 설계도, 빵틀, 개념, 인스턴스는 해당 설계를 바탕으로 구현된 실체로 정의할 수 있다. 인클래스는 다른 클래스를 상속할 수 있고, 여러 클래스를 상속할 수 있다. 프로그래밍 언어에서 클래스는 추상적인 대상일 수 있지만 구체적인 개체로 작동하기도 한다. 클래스 속성, 클래스메서드를 통해서 정의된 클래스와 구체적인 상호작용이 가능하기 때문이다. 현실에서는 과일이라는 전체 클래스를 대상으로 상호작용 할 수 없고, 과일 -> 사과 -> 내 손의 사과로 실체화된 내 손의 사과만을 대상으로만 상호작용 가능하다.

자바스크립트의 클래스

생성자함수 Array의 prototype에 정의된 특성은 생성자함수로 호출된 인스턴스가 상속받을 수 있다. 상속가능한 메서드는 클래스의 인스턴스 메서드와 유사하지만 자바스크립트에서는 프로토타입 메서드라 부르는 것이 좀 더 정확하다.

정확하게 말하면 프로토타입체이닝에 의한 참조다. 인스턴스의 __proto__ 프로퍼티가 생성자함수에 정의된 prototype속성을 참조하는 것

var Rectangle = function (width, height) {
  this.width = width;
  this.height = height;
};
Rectangle.prototype.getArea = function () {
  return this.width * this.height;
};
Rectangle.isRectangle = function (instance) {
  return instance instanceof Rectangle &&
    instance.width >0 && instance.height >0;
};

var rect1 = new Rectangle(3, 4);
console.log(rect1.getArea()); // 12
console.log(rect1.isRectangle(rect1)); // Error
console.log(Rectangle.isRectangle(rect1)); // true

클래스 상속

기본 구현

아래의 코드에서는 유사배열객체 Grade 생성자 함수를 구현했다. 하지만 length 프로퍼티를 삭제 및 제어 가능하고, Grade.prototype이 빈 배열을 참조하므로 잘 구현된 생성자 함수는 아니다. 클래스는 인스턴스가 참조하는 추상적 틀로서 잘 기능하도록 구현되어야 한다.

var Grade = function () {
  var args = Array.prototype.slice.call(arguments)
  for (var i = 0; i < args.length; i++) {
    this[i] = args[i];
  }
  this.length = args.length;
}

Grade.prototype = []; // Array 내장 클래스를 상속받는 Grade 클래스
var g = new Grade(100, 80)

사용자 정의 클래스 구현과 두 문제점

직사각형과 정사각형의 사용자 정의 클래스는 간단히 아래처럼 구현 가능하다. 얼핏 보면 하지만 많은 문제점을 가지고 있다.
교재의 안내에 따라서 아래의 코드를 안정적이고 잘 동작하는 클래스 구현으로 바꿔 보자.

처음 상태

var Rectangle = function (width, height) {
  this.width = width;
  this.heigth = height;
};
Rectangle.prototype.getArea = function () {
  return this.width * this.height;
};
var rect = new Rectangle(3, 4);
console.log(rect.getArea());

var Square = function (width) {
  this.width = width;
  this.height = width;
};
Square.prototype.getArea = function () {
  return this.width * this.width;
}
var sq = new Square(5);
console.log(sq.getArea());

상속을 통한 중복 제거

Square는 width === height 를 만족하는 Rect이므로 아래와 같이 바꿀 수 있다.


var Square = function (width) {
  Rectangle.call(this, width, width) // this.width, this.height에 width가 바인딩되도록 해줌.
};
Square.prototype = new Rectangle() 
// Rect 인스턴스를 prototype이 참조함으로서 rect1.__proto__가 가리키는 Rect의 prototype을 Square.prototyp이 참조하도록 해줬다. 상속의 구현!

아까보단은 발전된 코드지만 여전히 두 가지 문제가 있다. 첫번째 문제는 아래의 그림과 같이 Rectangle의 인스턴스 데이터필드까지 Square가 참조하게 된다는 점이다. 만약 Square의 width가 삭제되면 프로토타입 체이닝에 의해 Rectangle의 width를 참조하게되는 의도하지 않는 작동이 발생하게 된다.

var Square = function (width) {
  Rectangle.call(this, width, width) // this.width, this.height에 width가 바인딩되도록 해줌.
};
Square.prototype = new Rectangle(99, 99) 

sq1 = new Square(10);
delete sq1.width;
console.log(sq1.width); // 99

이런 의도하지 않은 결과가 나온다. (실제로는 이렇게 사용하지 않겠지만 이론적으로 안전한 코드는 아니다.)
인스턴스의 데이터까지 상속받기(정확히는 __proto__가 참조) 때문있다.

데이터 없는 클래스의 구현

방법 1 : 브루트 포스

가장 직관적인 방법으로는 잘못 주입된 데이터를 직접 삭제해주고, 객체를 동결하면 된다. 속성이 많다면 함수를 정의해서 아래의 알고리즘을 수행해준다. 하지만 다른 좋은 방법도 있으므로 실무에서 활용하지는 않을 듯하다.

delete Square.prototype.width;
delete Square.prototype.height;
Object.freeze(Square.prototype);

방법 2 : 브릿지

브릿지를 활용하는 더글라스 크락포드의 방법은 다음과 같다.
SubClass의 prototype에 직접 SuperClass의 인스턴스를 할당하는 대신 아무런 프로퍼티를 생성하지 않는 빈 생성자 함수(브릿지)를 하나 더 만들어서 브릿지의 prototype이 superClass의 prototype을 바라보게 한 다음 subClass의 prototype에는 브릿지의 인스턴스를 할당하게 하는 방법이다.

브릿지의 인스턴스에는 데이터필드가 없으므로, 브릿지의 인스턴스를 참조하는 과정은 상위 클래스의 인스턴스를 직접 참조할때 발생하는 인스턴스 데이터필드 상속을 막아준다.

var Rectangle = function (width, height) {
  this.width = width;
  this.height = height;
}

Rectangle.prototype.getArea = function () {
  return this.width * this.height;
}

var Square = function (width) {
  Rectangle.call(this, width, width);
};

var Bridge = function () {};
Bridge.prototype = Rectangle.prototype;
Square.prototype = new Bridge();
Object.freeze(Square.prototype);

위 로직을 함수화하면 다음과 같다.
즉시실행함수 내부에서 Bridge를 선언하고, 이를 클로저로 활용함으로써 메모리에 불필요한 함수 선언을 줄였다. subMethods에는 subClass의 prototype에 담길 메서드들을 객체로 전달하게끔 했다.

var extendClass2 = (function() {
  var Bridge = function () {};
  return function (SuperClass, SubClass, subMethods) {
    Bridge.prototype = SuperClass.prototype;
    Subclass.prototype = new Bridge();
    if (subMethods) {
      for (var method in subMethods) {
        SubClass.prototype[method] = subMethods[method];
      }
    }
    Object.freeze(SubClass.prototype);
    return SubClass;
  };
})();

방법 3 : Object.create()

ES5에서 도입된 Object.create()를 활용하는 것이 직관적이면서 간단하고, 안전하다.
실무에서 쓰려면 이렇게 하지 않을까?

Square.prototype = Object.create(Rectangle.prototype);
Object.freeze(Square.prototype);

생성자 이슈

profile
I'm going from failure to failure without losing enthusiasm

0개의 댓글