프로토타입과 클래스

코딩덕·2023년 6월 16일

💡 JS는 무슨 언어일까?

자바스크립트는 클래스 기반 객체지향 언어(Java, C++)보다 강력한 프로토타입 기반 객체지향 언어이다.

ES6에서 클래스가 도입되었다.
이를 통해 프로토타입의 생성자-프로토타입 패턴 대신, 가독성이 좋은 class 문법을 사용할 수 있게 되었지만, 내부적으로는 여전히 프로토타입 체인을 기반으로 동작하기 때문에 프로토타입에 대해 이해해야 한다.

객체지향 프로그래밍

프로그램을 수많은 객체라는 기본 단위로 나누고 그 객체들 간의 상호작용을 통해 로직을 구성하는 방식

프로토타입을 사용하는 이유는 객체 간의 공통 기능을 재사용해서, 상속을 메모리 사용면에 있어 효율적으로 하기 위해서다.


✅ 상속을 사용하지 않은경우

프로토타입을 이용하지 않고, 같은 getArea함수의 기능을 사용하기위해 circle1과 circle2에게 각각 새로운 getArea함수가 할당되었다.

이런 동일 메서드 중복생성은 불필요한 메모리 낭비를 야기한다.

function Circle(radius) {
  this.radius = radius;
  this.getArea = function () {
    return 3.14 * (this.radius ** 2);
  };
}

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

console.log(circle1.getArea === circle2.getArea); // false (각각 다른 getArea)
console.log(circle1.getArea()); 
console.log(circle2.getArea()); 

✅ 상속을 사용한 경우

하지만 프로토타입을 이용하면, Circle 생성자 함수가 생성하는 모든 인스턴스는 하나의 getArea함수를 공유 할 수 있다!

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

// getArea함수를 Circle프로토타입에 추가
Circle.prototype.getArea = function () {
  return 3.14 * (this.radius ** 2);
};

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

console.log(circle1.getArea === circle2.getArea); // true (공유된 getArea)
console.log(circle1.getArea()); 
console.log(circle2.getArea()); 

생성자-프로토타입 방식

먼저, 우리는 수많은 유사한 객체를 유지보수하고 관리하고 싶을 때 앞서 사용했던 생성자 함수(new) 를 사용한다! (이때 new 뒤에 함수는 대문자로 시작해야한다)

function Create(name, sport){
  this.name = name;
  this.sport = sport;
}

let player1 = new Create('커리', '농구');
let player2 = new Create('르브론', '농구');

그런데 이때 객체에 weight라는 속성을 추가하고 싶다면 2가지 방식이 있다.

  1. 생성자 Create안에 this.weight 코드를 직접 추가해주는 방법(static 메서드)
  2. 생성자 Create의 프로토타입에 추가해 주는 방식(instance 메서드)

이때 1번의 경우, 자식들이 weight 속성을 직접 가지는 반면,
2번의 경우는 부모만 weight 속성을 가진다.

따라서 2번의 경우에는 자식들이 부모의 속성인 weight를 마음대로 가져다 쓸 수 있지만 출력해보면 직접 weight를 가지고 있진않다!


✅ 프로토타입으로 상속 사용코드

// 부모
function Create(name, sport){
  this.name = name;
  this.sport = sport;
}

Create.prototype.weight = '90kg'

// 자식
let player1 = new Create('커리', '농구');
let player2 = new Create('르브론', '농구');

console.log(player1);  // Create {name:'커리', sport:'농구'} - weight 없음
console.log(player1.weight);   // 90kg 

// 프로토타입 출력하기
console.log(player1.__proto__); // { weight: '90kg' }
console.log(Create.prototype); // { weight: '90kg' }
console.log(player1.__proto__ === Create.prototype); // true

이렇게 player1.weight라는 값을 찾을 때 JS는 다음과 같은 동작을 한다.

  • player1에 weight가 있는지
  • player1의 부모의 프로토타입에 weight가 있는지(생성자 Create에 weight가 있는지) => 지금은 여기서 멈춤
  • player1의 부모의 부모의 프로토타입에 weight가 있는지
    .....

위처럼 객체의 프로토타입을 따라가며 상속을 구현하는 메커니즘을 프로토타입 체인이라고 한다.




클래스

ES5전까진 프로토타입을 통해 객체지향을 구현했지만,
ES6부턴 좀더 JAVA스럽게 객체 지향적으로 표현하기 위해 클래스(Class)가 도입되었다.

다만 생김새만 클래스 구조이지, 엔진 내부적으로는 프로토타입 방식으로 작동된다.
이 둘은 같은 결과를 출력하지만, 문법 생김새만 다르고 내부 로직은 완전히 같은 구조라는 점만 기억하면 된다.

✅ ES5 프로토타입

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

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

// 정적 메서드
Person.sayHi = function(){
  console.log('안녕! 코린아');
}

let person = new Person("Dongkeun");

person.sayName(); // Hi Dongkeun
person.sayHi(); // ERROR
Person.sayHi(); // 안녕! 코린아

✅ ES6 클래스

class Person {
  // 생성자 함수 => constructor
  constructor(name){
    this.name = name;
  }
  
  // 프로토타입 메서드 => 일반객체 메서드선언방식
  sayName(){
    console.log(`Hi ${this.name}`);
  }
  
  // 정적 메서드 => static
  static sayHi(){
    console.log('안녕! 코린아');
  }
}
  
let person = new Person("Dongkeun");

person.sayName(); // Hi Dongkeun
person.sayHi(); // ERROR
Person.sayHi(); // 안녕! 코린아


클래스 문법

constructor

class Person {
  // 생성자
  constructor(name, age){
    // 인수로 인스턴스 초기화
    this.name = name;
    this.age = age;
    // 고정값으로 인스턴스 초기화
    this.address = 'Seoul';
  }
}

let person = new Person('Dongkeun', 27);  // 인스턴스 생성

console.log(person); // Person {name: 'DongKeun', age: 27 , address: 'Seoul'}
  • 클래스 이름은 항상 대문자로 시작한다.
  • 클래스를 통해 생성된 객체를 인스턴스(instance) 라고 부른다.
  • constructor는 인스턴스를 생성하고 초기화하는 특수한 메서드이다.
  • constructor는 클래스 안에 반드시 한 개만 존재할 수 있다.
  • constructor생략할 수 있다. (하지만 인스턴스를 초기화하려면 constructor를 생략하면안된다)
  • constructor는 class에서 필요한 기초 정보를 세팅하는 곳이다. 객체를 new로 생성할 때 가장먼저 자동으로 호출된다.

(추가) constructor 생략가능

class Person2 {}

let person2 = new Person();  // 인스턴스 생성

console.log(person2); // Person2 {}

인스턴스(일반) 메서드 선언방식

class Person {
  constructor(name, age){
    this.name = name;
    this.age = age;
  }
  
  sayName(){
    console.log(`Hi I'm ${this.name}, ${this.age} years old`);
  }
}

let person = new Person("Dongkeun", 27);  
person.sayName(); // Hi I'm DongKeun, 27 years old
  • 프로토타입 방식에서 Person.prototype.sayName으로 만들었던 프로토타입 메서드와 같다.
  • 하지만 클래스 방식에선 클래스안에서 일반 객체메서드 선언방식으로 선언한다.

정적 메서드 선언방식

class Person {
  constructor(name){
    this.name = name;
    this.age = 27;
  }
  
  static sayHi(){
    console.log('안녕! 코린아');
  }
  
  static sayError(){
    console.log(this.name, this.age);
  }
}
  
let person = new Person("Dongkeun");

person.sayHi(); // ERROR
Person.sayHi(); // 안녕! 코린아
Person.sayError(); // Undefined
  • 프로토타입 방식에서 명시적으로 추가해서 만들었던 정적 메서드와 같다.
  • 하지만 클래스 방식에선 클래스안에서 static으로 선언한다.
  • static 구문안에서는 constructor에서 선언한 this값(name, age)이 먹히지 않는다!!

static과 인스턴스 비교

class Compare {
  // static 
  static staticProp = 'staticProp';
  static staticMethod() {
    return 'Im a static method.';
  }
  // instance
  instanceProp = 'instanceProp';
  instanceMethod() {
    return 'Im a instance method.';
  }
}

// 상속하면 부모의 static요소들을 사용 가능
console.log(Compare.staticProp); // staticProp
console.log(Compare.staticMethod()); // Im a static method.
console.log(Compare.instanceProp); // Undefined
console.log(Compare.instanceMethod()); // ERROR

// 상속하면 부모의 인스턴스를 사용 가능
const c = new Compare();
console.log(c.staticProp); // Undefined
console.log(c.staticMethod()); // ERROR
console.log(c.instanceProp); // instanceProp
console.log(c.instanceMethod()); // Im a instance method.


클래스 상속

클래스 상속 기능을 통해 한 클래스의 기능을 다른 클래스에서 재사용할 수 있다.

✅ extend

class Parent {
  constructor (name, age, city) {
    this.name = name;
    this.city = city;
  }
  
  nextYearAge() {
    return Number(this.age) + 1;
  }
}

class Child extends Parent {
  introduce () {
        return `저는 ${this.city}에 사는 ${this.name} 입니다.`
  }
}

const c = new Child('Lee', 27, 'seoul');

console.log(c.introduce()); // 저는 seoul에 사는 Lee 입니다.

extends 키워드를 통해 Child 클래스가 Parent 클래스를 상속 했다.
상속을 이용하면 부모의 class의 값을 모두 접근하여 사용할 수 있다.

이 관계를 보고 부모 클래스-자식 클래스 관계 혹은 슈퍼 클래스-서브 클래스 관계라고 한다.

✅ super

class Parent {
  constructor (name, age, city) {
    this.name = name;
    this.age = age;
    this.city = city;
  }
    
  nextYearAge() {
    return Number(this.age) + 1;
  }
}

class Child extends Parent {
  constructor(name, age, city, futureHope) {
    super(name, age, city);  // 부모 생성자 가져옴
    this.futureHope = futureHope
  }
  introduce () {
    // 부모의 nextYearAge() 메서드 가져옴
    return `저는 ${this.city}에 사는 ${this.name} 입니다. 
    		내년엔 ${super.nextYearAge()}살이며, 
    		장래희망은 ${this.futureHope} 입니다.`
  }
}

let c = new Child('Lee', 27, 'seoul', '개발자');

console.log(c.introduce()); // 저는 seoul에 사는 Lee입니다. 내년엔 28살이며, 장래희망은 개발자 입니다.

부모 class 값을 상속받고, 추가적으로 자식만의 값을 사용하고싶다면 super 키워드를 사용할 수 있다.

  • 생성자 내부에서 super를 함수처럼 호출하면, 부모 클래스의 생성자가 호출
  • super.prop과 같이 써서 부모 클래스의 prop 인스턴스 속성에 접근할 수 있다.

🔥프로토타입 vs 클래스

특징프로토타입 기반class 문법
문법복잡하고 가독성이 낮음직관적이고 객체지향적
생성자 함수 호출new 없이 호출 가능new 없이 호출 불가
상속Object.create() 또는 prototype 활용extends 사용 가능
정적 메서드직접 함수 추가 필요static 키워드 제공

0개의 댓글