클래스

김동영·2025년 4월 4일

자바스크립트

목록 보기
7/7
post-thumbnail

자바스크립트 클래스

자바스크립트는 프로토타입 기반 언어라서 ‘상속’개념이 존재하지 않습니다!
그래서 다른언어에 익숙한 개발자들을 혼란스럽게 만들었고
이를 해결하기 위해 ES6에서 클래스 문법이 추가 되었습니다.

하지만! 이부분도 프로토타입을 활용하여 구현한것입니다!
그래서 이번 정리는 클래스가 어떤식으로 구현이 되었는지 한번 확인해 보겠습니다!

예를들어 생성자 함수 Array를 new를 통해 arr라는 인스턴스를 만들었다고 생각해 보겠습니다.

  • 이때 Array 를 일종의 class로 본다면 Array의 prototype 객체 내부 요소들이 arr로 상속된다고 보는 것입니다. 물론 프로토타입 체이닝에 의한 참조입니다.
  • 또한  Array의 prototype을 제외한 메서드는 arr에 상속되지 않습니다.

또 다른 예제를 통해 확인 해보겠습니다.

// 생성자
const 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;
};

const rect1 = new Rectangle(3,4);

console.log(rect1.getArea()); // 12
console.log(rect1.isRectangle()); // TypeError: rect1.isRectangle is not a function
console.log(Rectangle.isRectangle(rect1)); // true
  • 다음과 같이 인스턴스 rect1에서 프로토타입 체이닝을 통해 getAreat()를 바로 호출할 수 있는 메서드를 프로토타입 메서드입니다.
  • 인스턴스 rect1에서 프로토타입 체이닝으로 직접 접근할 수 없는 메서드 isRectangle()스태틱 메서드 입니다.

클래스 상속

그래서 ES5에서는 클래스가 없기 때문에 이런 프로토타입 체이닝을 통해 클래스 상속을 구현한 것입니다.

직접 확인하면서 클래스를 직접 구현했을때 어떤 문제가 있는지 확인해 보겠습니다.

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

g.push(90);
console.log(g); // Grade { 0: 100, 1: 80, 2: 90, length: 3 }

delete g.length;
g.push(70);
console.log(g); // Grade { 0: 70, 1: 80, 2: 90, length: 1 }
  • 처음 90을 push 한것은 정확하게 나왔지만
    length 속성이 수정 가능한 점과 Grade.prototype가 빈배열을 참조 한다는 점이 문제가 발생하게 된 원인입니다.
  • 원래 배열은 length 속성이 삭제가 안되지만 삭제가 가능하게 되어 g.length 읽어올라 했지만 사라져서 프로토타입 체이닝을 통해 g.__proto__.length를 읽어 왔기때문에 빈배열(Grade.prototype)의 length 0을 읽게되 잘못되게 push가 된 것 입니다.

이렇기 때문에 클래스에 있는 값이 인스턴스의 동작에 영향을 미치면 안됩니다!!

또다른 예제를 확인해 보겠습니다.

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

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

const rect = new Rectangle(3,4);
console.log(rect.getArea()); // 12

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

Square.prototype = new Rectangle()

const sq = new Squeare(5);
console.log(sq.getArea()); // 25

다음과 같이 직사각형과 정사각형 클래스를 만들어서 공통된 메서드를 사용하기 위해 Squre 의 프로토타입 객체에 Rectangle의 인스턴스를 부여했습니다. (마치 클래스 상속)

물론 동작은 제대로 하지만 문제점이 있습니다!

클래스의 값때문에 인스턴스에 영향이 미치는것을 확인할 수 있습니다!

sq 의 구조를 보게 되면 sq.__proto__ 는 Rectengle의 인스턴스를 바라보게 되어
width와 height가 undefined로 되어있는것을 확인 할 수 있습니다.
Square.prototype 에 값이 존재 하여 임의로 Square.prototype.width 에 값을 부여하여 sq.width를 지워 버리면 프로토타입 체이닝에 의해 이상한 값이 나오게 될 것입니다!

그렇기 때문에 이러한 문제를 해결하기 위해 클래스가 구체적인 데이터를 지니지 않게 해야합니다!

클래스가 구체적인 데이터를 지니지 않게 하는 방법

1) 인스턴스 생성 후 프로퍼티 제거하기

const extendClass1 = function(SuperClass, SubClass, subMethods) {
  SubClass.prototype = new SuperClass();
  for (const prop in SubClass.prototype) {
    if (SubClass.prototype.hasOwnProperty(prop)) {
      delete SubClass.prototype[prop];
    }
  }
  if (subMethods) {
    for (const method in subMethods) {
      SubClass.prototype[method] = subMethods[method];
    }
  }
  Object.freeze(SubClass.prototype);
  return SubClass;
};

const Rectangle = function(width, height) {
  this.width = width;
  this.height = height;
};
Rectangle.prototype.getArea = function() {
  return this.width * this.height;
};
const Square = extendClass1(Rectangle, function(width) {
  Rectangle.call(this, width, width);
});
const sq = new Square(5);
console.log(sq.getArea()); 

간단하게 설명하자면

  • extendClass1에서 SuperClass의 인스턴스를 SubClass.prototype으로 설정하여 상속을 구현했습니다. 이후 SubClass.prototype 에 직접 정의된 프로퍼티를 삭제하여 subMethods 객체를 받아 SubClass.prototype에 원하는 메서드를 추가합니다. 마지막으로 Object.freeze(SubClass.prototype)를 사용하여 SubClass.prototype을 변경할 수 없도록 만들었습니다.

2) 빈 함수(Bridge)를 활용하기

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

const Rectangle = function(width, height) {
  this.width = width;
  this.height = height;
};
Rectangle.prototype.getArea = function() {
  return this.width * this.height;
};
const Square = extendClass2(Rectangle, function(width) {
  Rectangle.call(this, width, width);
});
const sq = new Square(5);
console.log(sq.getArea()); 
  • Bridge라는 빈 함수를 만들어서 Bridge.prototypeRectangle.prototype을 바라보게끔 한 다음 Square.prototypeBridge의 인스턴스를 할당하여
    Rectangle 자리에 Bridge가 대체하게 되어 구체적인 데이터가 남아 있지 않습니다.

3)Object.create 이용하기

const Rectangle = function(width, height) {
  this.width = width;
  this.height = height;
};
Rectangle.prototype.getArea = function() {
  return this.width * this.height;
};
const Square = function(width) {
  Rectangle.call(this, width, width);
};
Square.prototype = Object.create(Rectangle.prototype);
Object.freeze(Square.prototype);

const sq = new Square(5);
console.log(sq.getArea());
  • SubClassprototype의 __proto__SuperClassprototype을 바라보되
    SuperClass의 인스턴스가 되지 않아서 해결하는 방법입니다.

constructor 복구하기

  • 위에 방법은기본적인 상속은 성공했지만 SubClass 인스턴스의 constructor는 SuperClass를 가르키는 상태입니다. 즉 constructor가 다른 부분을 가르키고 있기 때문에 수동으로 올바르게 연결해야 합니다.
const extendClass1 = function(SuperClass, SubClass, subMethods) {
  SubClass.prototype = new SuperClass();
  for (const prop in SubClass.prototype) {
    if (SubClass.prototype.hasOwnProperty(prop)) {
      delete SubClass.prototype[prop];
    }
  }
  SubClass.prototype.consturctor = SubClass; // 수동으로 올바르게 가르키게 변경 
  if (subMethods) {
    for (const method in subMethods) {
      SubClass.prototype[method] = subMethods[method];
    }
  }
  Object.freeze(SubClass.prototype);
  return SubClass;
};
profile
안녕하세요 프론트엔드개발자가 되고싶습니다

0개의 댓글