본문 바로가기

자스핑

[Week 05] 프로토타입과 클래스 상속

 

안녕하세요 YB 김고은입니다! 

오늘은 19장 전반에 걸친 프로토타입과, 25장 일부에 나와있는 클래스 상속을 다뤄보도록하겠습니다

책의 범위는 다음과 같습니다. 

 

프로토타입 19장 (p.260-311)

클래스 상속 25장 (p.448 - 466 ) 

 

특히, 프로토타입의 경우에 객체 상속이 포인트인거 같아서

프로토타입으로 상속하는 이유랑 간접 참조를 위주로 작성해보겠습니다

클래스 상속의 경우, 상속 주고 받는 형식을 위주로 작성했습니다 :) 


 프로토타입

객체 지향 프로그램 언어인 자바스크립트의 핵심 개념인 상속은, 

어떤 객체의 프로퍼티 또는 메서드를 다른 객체가 상속 받아 그대로 사용할 수 있는 것을 말한다

 

이때 JS는 프로토타입을 기반으로 상속을 구현해서, 중복을 제거하는데 이 방식에서 기존의 코드를 적극 활용하는 식으로 코드를 재사용한다. 

 

생성자 함수에서, 프로퍼티와 메소드 구조를 갖는 객체가 있다고 하자. 코드는 다음과 같다! 

 

// 생성자 함수
function Circle(radius) {
  this.radius = radius;
  this.getArea = function () {
    return Math.PI * this.radius ** 2;
  };
}


const circle1 = new Circle(1);
const circle2 = new Circle(2);

// 각 인스턴스는 서로 다른 getArea 메서드를 소유하게 된다.
console.log(circle1.getArea === circle2.getArea); // false


console.log(circle1.getArea()); // 3.141592653589793
console.log(circle2.getArea()); // 12.566370614359172

 

생성자 함수를 생성할때마다, 동일한 동작을 하는 메서드가 중복 생성되게 되고,  인스턴스마다 이를 중복 소유한다. 

이렇게 되면 메모리 낭비가 발생하고, 인스턴스 생성할때마다 메서드가 생성되기 때문에 성능에도 악영향을 준다 

(인스턴스를 10개 생성하면, 동일한 메서드도 10개 생겨남)

 

 

따라서, 이러한 불필요한 중복 제거를 위해서, JS는 프로토타입을 기반으로 상속을 구현한다.

 

function Circle(radius) {
  this.radius = radius;
}

Circle.prototype.getArea = function () {
  // Math.PI는 원주율을 나타낸다.
  return Math.PI * this.radius ** 2;
};


const circle1 = new Circle(1);
const circle2 = new Circle(2);

// Circle 생성자 힘수가 생성하는 모든 인스턴스는, 단 하나의 getArea 에서드를 공유
console.log(circle1.getArea === circle2.getArea); // true

console.log(circle1.getArea()); // 3.141592653589793
console.log(circle2.getArea()); // 12.566370614359172

 

Circle 생성자 함수로 부터 비롯된 모든 인스턴스는, 

부모 객체의 역할을 하는 프로토타입으로부터 getArea 메서드를 상속 받기 때문에, 하나의 메서드를 공유하게 된다!! 

 

 

프로토타입은 어떤 객체의 상위 객체의 역할을 하는 객체로서 다른 객체에 공유 프로퍼티를 제공하기에, 상속이 가능하다! 

 

모든 객체는 [[ Prototype ]] 이라는 내부 슬롯을 가지는데, 이 내부 슬롯 값은 프로토타입의 참조이다! 

[[ Prototype ]] 에 저장되는 프로토타입은 객체 생성 방식에 의해서 결정되게 된다.

 

 

(예를 들어/ 객체 생성 방식에 의해 결정)

객체 리터럴에 의해 생성된 객체 프로토타입 === Object.prototype 이며,

함수에 의해 생성된 객체 프로토타입은 === 생성자 함수의 prototype 에 바인딩되어 있는 객체 

 

모든 객체하나의 프로토타입을 갖고, 모든 프로토타입은 생성자 함수와 연결되어 있다. 

 

[[Prototype]] 내부 슬롯에는 직접 접근 할 수는 없다. 내부 슬롯은 프로퍼티가 아니기 때문! 

하지만, __proto__ 접근자 프로퍼티를 통해서 자신의 프로토타입에 간접적으로 접근 가능하다. (자신의 내부 슬롯을 가리키는 프로토타입을 통해서 접근 )

 

왜 이렇게 간접 접근만을 허용했는가? 

- 상호 참조에 의한 프로토타입 체인이 생성되는 것을 방지하기 위해서 라고 한다

 

이때 __proto__ 는 접근자 함수 getter/setter 를 통해서, 프로토타입 값을 취득하거나 할당한다. 

( getter/setter 는 [[ Get ]]  [[Set]]  프로퍼티 어트리뷰트에 할당된 함수)

__proto__ 에 접근하면 getter 함수가 호출되고, __proto__  접근자 프로퍼티에 새로운 프로토타입을 할당하게되면 setter 함수가 호출된다. 

 

const obj = {};
const parent = { x: 1 };


// getter 함수 get __proto__를 사용하여 obj 객체의 프로토타입 취득
obj.__proto__;

// setter 함수 set __proto__를 사용하여 obj 객체의 프로토타입 교체
obj.__proto__ = parent;


console.log(obj.x); // 1

 

여기에서 prototype__proto__ 접근자 프로퍼티 비교해보기

 

 

프로토타입 체인 

function Person(name) {
  this.name = name; // 생성자 함수
}

Person.prototype.sayHello = function () { // 프로토타입 메서드
  console.log(`Hi! My name is ${this.name}`);
};

const me = new Person('Lee'); // 인스턴스 

console.log(me.hasOwnProperty('name')); // true

 

생성자 Person 함수로, 생성한 me 객체는 메서드 hasOwnProperty를 호출할 수 있다 (Object.prototype의 메서드)

이것은 Person.prototype 뿐만 아니라, Object.prototype도 상속받았다는 걸의미한다. 

(사실 Person 생성자로 만들어졌지만, 체이닝에 의해 Object 도 연결되어 있음을 알 수 있음 )

 

// 어떤 프로토타입이든, 프로토타입은 언제나 Object.prototype!

Object.getPrototypeOf(me) === Person.prototype; // -> true

Object.getPrototypeOf(Person.prototype) == Object.prototype; // -> true

 

.getPrototypeOf(A) 는 객체 A 의 부모 객체 프로토타입를 확인하기 위한 메소드  

 

자바스크립트에서 객체 프로퍼티에 접근하려 할때, 객체에 접근하려는 프로퍼티가 없다면 [[ Prototype ]] 내부 슬롯의 참조를 따라, 자신의 부모역할을 하는 프로토타입의 프로퍼티를 순차적으로 검색하는데 이 과정을 null 이 나올때까지 한다고 한다. 

 

 


클래스

 

클래스에 대한 간단 특징 

원래 자바스트립트는 프로토타입을 기반으로한 객체 지향 언어인데,

ES6 에서 도입된 클래스를 통해 기존의 프로토타입 기반 패턴을 클래스 기반 패턴처럼 사용할 수 있도록 되었다

 

클래스는 생성자 함수와 유사하게 동작하지만 차이점이 존재하기도 한다.

 

1. 클래스를 new 연산자 없이 호출하면 에러가 발생,
반면, 생성자 함수를 new 연산자 없이 호출하면 일반 함수로서 호출된다 

2.  클래스는 상속을 지원하는 extends와 super 키워드를 제공,
반면, 생성자 함수는  extends와 super  제공하지 않는다 

3. 클래스는 호이스팅이 발생하지 않는 것처럼 동작하지만,
생성자 함수(함수 선언문으로 정의된 함수)는 함수 호이스팅이 / 표현식으로 정의한 함수는 변수 호이스팅이 발생

4.   클래스 내의 모든 코드는 암묵적으로 strict mode가 지정되고, 이를 헤제 불가하지만, 
생성자 함수는 암묵적으로 strict mode가 지정되지 않는다 

5. 클래스의 constructor, 프로토타입 메서드, 정적 메서드는 열거 되지 않는다

 

상속에 의한 클래스 확장 

상속에 의한 클래스 확장은 지금까지의 상속과는 조금 다른데, 

 

프로토 타입 기반의 상속은 프로토타입 체인을 통해, 다른 객체의 자산을 상속받는 개념이지만, 
상속에 의한 클래스 확장은 기존 클래스를 상속 받아, 새로운 클래스를 확장하여 정의하는 것이다. 

 

클래스의 경우, extend 연산을 통해서 확장이 가능하다. 예제 코드는 아래와 같다

 

class Animal { // 클래스 'animal' 정의!
  constructor(age, weight) {
    this.age = age;
    this.weight = weight;
  }

  eat() { // 메서드 1 
    return 'eat';
  }
 
  move() { // 메서드 2 
    return 'move';
  }
}

class Bird extends Animal { // Animal 클래스를 상속한 -> Bird 클래스 정의
  fly() {
    return 'fly';
  }
}

// Bird 인스턴스 생성
const bird = new Bird(1, 5);

console.log(bird); // Bird { age: 1, weight: 5 }
console.log(bird instanceof Bird); // true
console.log(bird instanceof Animal); // true
console.log(bird.eat()); // eat
console.log(bird.move()); // move
console.log(bird.fly()); // fly

 

이때 상속을 통해 확장된 클래스를 서브 클래스라고 하고,  (aka 파생 클래스 / 자식 클래스 ) 

서브클래스에게 상속된 클래스를 수퍼 클래스라고 부른다.  (aka 베이스 클래스 / 부모 클래스)

 

동적 상속 

extends 키워드를 통해, 클래스뿐만 아니라 생성자 함수를 상속받아 클래스를 확장할 수도 있다 

또한, 조건에 따라서 동적으로 상속 대상을 결정할 수도 있다

(단, extends 키워드 앞에는 반드시 클래스가 와야함!) 

 

생성자 함수를 확장한 경우 

function Base(a) { // 생성자 함수 정의
  this.a = a;
}

// 생성자 함수 Base를 상속받는 =>  Derived 클래스
class Derived extends Base {}


const derived = new Derived(1);
console.log(derived); // Derived { a: 1 }

 

동적으로 상속 받는 경우 

function Base1() {}
class Base2 {}

let condition = true;

// 삼항연산자를 통해 동적으로 확장!
class Derived extends (condition ? Base1 : Base2) {}


const derived = new Derived();

console.log(derived); // Derived {}
console.log(derived instanceof Base1); // true
console.log(derived instanceof Base2); // false

 

Constructor와  suepr 

수퍼클래스의 constructor 로 값을 전달하면 => 자식 클래스의 super 에서  상속 받는 형식이다

 

super 키워드는 함수처럼 호출도 가능하고, this 와 같이 식별자처럼 참조할 수 있게 하는 키워드인데 하는 기능은 크게 두가지이다

 

1️⃣ super 를 호출하면 수퍼클래스의 constructor 를 호출하고 

2️⃣ super를 참조하면 수퍼클래스의 메서드를 호출할 수 있다. 

 

 

super 호출 시 주의사항 

1. 서브클래스에서 constructor를 생략하지 않는 경우!
서브클래스의 constructor에서는 반드시 super를 호출해야 한다.

2. 서브클래스의 constructor에서 super를 호출하기 전에는 this를 침조할 수 없다

3. super는 반드시 서브클래스의 constructor에서만 호출한다.
이때, 서브클래스가 아닌 클래스의 constructor나 함수에서 super를 호출하면 에러 발생 🚨