[모자딥] 클래스와 프로토타입

Seungrok Yoon (Lethe)·2023년 12월 29일
0

클래스와 프로토타입

이 파트는 19장 프로토타입을 공부하면서, 클래스와 연관지어 추가로 학습 후 정리한 내용이다.

흔히 자바스크립트에서 클래스 문법을 생성자 함수의 syntax sugar라고 일컫는다. 자바스크립트에서 class 문법을 사용해서 객체를 생성하는 것 또한, 내부적으로는 프로토타입으로 구현되어 있기 때문에 그렇게 알려져있다.

하지만 클래스는 생성자 함수를 통한 객체 생성에서 제공하지 않는 기능도 제공한다. 그렇기에 클래스는 프로토타입 기반 객체 생성 패턴을 동일한 기능으로 완벽히 대체하는 클래스 패턴에 익숙한 프로그래머들을 위한 문법적 설탕아니라, 새로운 객체 생성 메커니즘으로 간주하는 것이 합당하다.

아래 코드는 위 MDN링크에 예시코드이다. 클래스의 메서드는 인스턴스가 아니라, 인스턴스의 프로토타입(클래스.prototype)에 등록이 된다고 한다. 한 번 확인해보자.

class Box {
  constructor(value) {
    this.value = value;
  }

  // Methods are created on Box.prototype
  getValue() {
    return this.value;
  }
}

const boxInst = new Box(5);
console.log(boxInst.__proto__.getValue === Box.prototype.getValue); //true
console.log(Object.getOwnPropertyDescriptors(boxInst));
/**{
  value: { value: 5, writable: true, enumerable: true, configurable: true }
}
*/

위 코드의 결과는 흥미로웠다. 나는 메서드들이 인스턴스에 등록이 될 것이라 철썩같이 믿고 있었기 때문이다. 그렇지만 클래스 문법으로 작성한 객체의 메서드들은 모두 프로토타입에 생성되어 있었다.

그렇다면 extends를 통해서 상위 클래스를 확장하여 상속하는 경우에는,

  • A)상위 클래스의 속성들은(프로퍼티와 메서드들) 하위 클래스로 전달이 될까?
  • B)아니면 상위 클래스 속성들이 상위 클래스에 귀속된 채로 프로토타입 체인을 통해서 하위 클래스로부터 찾아나가는 걸까?

전자라면 생성자함수의 프로토타입체인과 동일한 방식으로 class 확장이 이루어지는 것이고, 후자라면 생성자함수의 프로토타입 기반 상속과는 다른 방식으로 class의 확장상속이 이루어지는 것이다.

클래스의 extends를 통한 상속과 프로토타입체인

extends 상속을 통한 명시적인 클래스 확장과 지금까지 살펴본 프로토타입 기반 상속은 같은 개념일까?

for...in 문 사용시 주의점

for... in문은 객체의 프로토타입 체인 상에 존재하는 모든 프로토타입의 프로퍼티 중에서 프로퍼티 어트리뷰트 [[Enumerable]]의 값이 true인 프로퍼티를 순회하며 열거(enumeration) 한다.

그래서 사용시에 주의해야 한다. 아래 코드를 살펴보자. Box클래스를 ShapeBox클래스가 확장하여 상속하고 있다. 마지막 for...in문의 출력값을 예상해보자.

나는 shape만 키값으로 출력될 것이라 생각했는데, shape, value 키가 함께 출력된다. Box의 프로퍼티인 size가 shapeBox의 고유 프로퍼티로 출력이 되고 있는 상황이다. 어떻게 된 일일까?

class Box {
  constructor(value) {
    this.size = value;
  }

  // Methods are created on Box.prototype
  getValue() {
    return this.size;
  }
}

class ShapeBox extends Box {
  constructor(shape) {
    super(5);
    this.shape = shape;
  }
}

const shapeBoxInstance = new ShapeBox("rectangle");

console.log(shapeBoxInstance.__proto__.__proto__ === Box.prototype);
for (const prop in shapeBoxInstance) {
  if (!shapeBoxInstance.hasOwnProperty(prop)) continue;
  console.log(prop, shapeBoxInstance[prop]);
}


//size, shape 가 출력된다?

명시적인 상속을 나타내주는 extends 키워드를 통해서 클래스를 상속하는 것은 생성자 함수를 기반으로한 프로토타입 기반 상속과는 다르다

클래스의 constructor 메서드 내부에 정의된 프로퍼티는 인스턴스 프로퍼티이다. 즉, 인스턴스가 가지고 있는 속성값이다. 따라서, Box클래스의 size속성은 ShapeBox의 인스턴스인 shapeBoxInstance가 가지게 된다.

그러나 메서드는 인스턴스가 아닌, 클래스의 프로토타입에 등록된다.

extends로 확장한 클래스 인스턴스를 생성(new~)할 때 어떤 일이 발생하는가?

수퍼클래스를 상속한 서브클래스는 new 연산자로 인스턴스를 생성할 때 암묵적으로 constructor가 호출이 되고, 인자가 전달이 된다.

constructor에 전달된 인자들은 그대로 super()로 전달이 되어 수퍼클래스의 constructor(super-constructor)를 호출하여 인스턴스를 생성한다.

만약 서브 클래스에서 추가적인 프로퍼티를 가지는 인스턴스를 생성하고자 한다면, 서브클래스에서 constructor메서드를 구현해주어야 한다.그리고, 그 내부에서 super()에 명시적으로 인자를 전달해줘야한다. 그렇게 하지 않으면 this를 참조할 수 없게된다.

서브클래스 호출 시 전달된 인수들은 수퍼클래스와 서브 클래스가 나눠가지고, 상속관계의 두 클래스가 협력하여 인스턴스를 생성한다.

서브클래스 인스턴스는 상위 클래스들의 프로퍼티를 상속하여 초기화 후에 인스턴스 프로퍼티로 가지고 있고, 메서드는 프로토타입 체인을 통해서 인스턴스들 간에 공유한다.

class Shape {
  constructor(vertices) {
    if (vertices.length === 0) throw new Error("no argument");
    this.vertices = vertices;
  }
  getVertices() {
    return this.vertices;
  }
}

class Triangle extends Shape {
  constructor(vertices, name) {
    super(vertices);
    this.name = name;
  }
  printInfo() {
    return super.getVertices() + " " + this.name;
  }
}

class EquilateralTriangle extends Triangle {
  printName() {
    console.log(this.name);
  }
}

const equiTriangle = new EquilateralTriangle(1, "Equi");
console.log(equiTriangle.printInfo()); //1 Equi
equiTriangle.printName(); //Equi

클래스 인스턴스의 프로토타입체인

우리가 생성자 함수를 예시로 들었던 프로토타입체인을 완벽하게 구현하고 있을까?

MDN의 예시는 클래스 extends를 사용하여 상속하는 경우 프로토타입 체인의 변화를 설명해주고 있다.

function Base() {}
function Derived() {}
// Set the `[[Prototype]]` of `Derived.prototype`
// to `Base.prototype`
Object.setPrototypeOf(Derived.prototype, Base.prototype);

const obj = new Derived();
// obj ---> Derived.prototype ---> Base.prototype ---> Object.prototype ---> null
class Base {}
class Derived extends Base {}

const obj = new Derived();
// obj ---> Derived.prototype ---> Base.prototype ---> Object.prototype ---> null

Box라는 생성자함수는 ShapBox함수의 프로토타입이다.
Box.prototype은 ShapeBox.prototype.__proto__이다. ShapeBox와 Box의 프로토타입체인을 도식으로 표현하면 다음과 같을 것이다.


class Box {
  constructor(value) {
    this.size = value;
  }

  // Methods are created on Box.prototype
  getValue() {
    return this.size;
  }
}

class ShapeBox extends Box {
  getSize() {
    return this.size ** 2;
  }
}

class ShapeBoxWithConstructor extends Box {
  constructor(size, shape) {
    super(size);
    this.shape = shape;
  }
  getSize() {
    return this.size ** 2;
  }
  getShape() {
    return this.shape;
  }
}

const shapeBoxInstance = new ShapeBox(4);
const shapeBoxWithConstructorInstance = new ShapeBoxWithConstructor(
  4,
  "rectangle"
);

for (const prop in shapeBoxInstance) {
  if (!shapeBoxInstance.hasOwnProperty(prop)) continue;
  console.log(prop, shapeBoxInstance[prop]);
}

for (const prop in shapeBoxWithConstructorInstance) {
  if (!shapeBoxWithConstructorInstance.hasOwnProperty(prop)) continue;
  console.log(prop, shapeBoxWithConstructorInstance[prop]);
}

console.log(shapeBoxInstance.size);
console.log(shapeBoxInstance.getSize());
console.log(shapeBoxWithConstructorInstance.size);
console.log(shapeBoxWithConstructorInstance.getShape());
profile
안녕하세요 개발자 윤승록입니다. 내 성장을 가시적으로 기록하기 위해 블로그를 운영중입니다.

0개의 댓글