본문 바로가기

Coding/TIL (Today I Learned)

[JavaScript] __proto__, constructor, prototype의 관계, 그리고 Class

자바스크립트가 가지고 있는 __proto__, constructor, prototype라는 용어부터 혼란스러운 이 세 가지의 개념과 관계에 대해 정리해 봅니다.

 

Prototype이란?

자바스크립트의 모든 객체는 부모 객체를 가지고 있으며 연결되어 있는데 이 때 부모 객체를 프로토타입 객체(또는 prototype)라고 합니다. 이로 인해 객체지향 프로그래밍의 상속 개념과 같이 부모 객체의 프로퍼티와 메서드를 사용할 수 있습니다. Prototype을 사용하는 이유는,

  • 생성자 함수로 생성된 모든 객체가 프로퍼티, 메서드를 공유할 수 있습니다.

  • 상속을 구현할 수 있습니다.

 

__proto__란? (prototype을 이용한 Class 흉내내기)

  • 객체를 생성하면 동시에 객체에는[[prototype]]라는 것이 생성됩니다. 따라서 모든 객체는 내부 프로퍼티 [[prototype]]가지고 있습니다. 이것은 함수 객체의 prototype 프로퍼티와는 다른 객체입니다. [[prototype]]의 값은 객체의 __proto__라는 property에서 접근이 가능합니다.

  • __proto__는 부모 함수의 prototype을 바라봅니다. 그런 이유로 prototype link라고도 부릅니다. __proto__라는 명칭은 일부 브라우저에서 사용하는 프로퍼티 명이며 ECMA명세서에는 [[Prototype]] 이라는 이름으로 사용합니다. 하지만 저는 크롬 브라우저 기준으로 공부를 하고 있으므로 이후로는 __proto__라고 사용하도록 하겠습니다.

  • 객체의 __proto__프로퍼티는 그 객체에 상속을 해 준 부모 객체를 가리킵니다. 그리고 객체는 __proto__가 가리키는 부모 객체의 프로퍼티를 사용할 수 있습니다. 이 특징을 이용하여 프로토타입 체이닝이 가능합니다.

  • Prototype Chaining이란 자바스크립트 엔진이 어떤 프로퍼티나 메서드에 접근하려고 할 때 해당 객체에 찾는 프로퍼티나 메서드가 없다면 __proto__가 가리키는 링크를 따라 부모 객체의 프로퍼티나 메서드를 차례대로 올라가며 찾아보는 것을 의미합니다. 체인의 종점은 Object.prototype입니다. 

  • 아래 예시 코드에서 objC.sayHello()가 호출되면 먼저 objC는 자신의 프로퍼티를 확인합니다. 하지만 찾을 수 없으므로 다음으로 objC.__proto__가 가리키는 objB의 프로퍼티를 확인합니다. 역시 objB 안에서도 찾을 수 없으므로 objB.__proto__.__proto__가 가리키는 objA에서 프로퍼티를 확인합니다. 드디어 sayHello를 발견했습니다. 이제 objA.sayHello를 사용합니다.

let objA = {
  name: "Tom",
  sayHello: function() {
    console.log("Hello!" + this.name);
  }
};

let objB = {
  name: "Nina"
};

let objC = {};

objB.__proto__ = objA; 
objC.__proto__ = objB;

objB.sayHello(); //Hello!Nina
objC.sayHello(); //Hello!Nina

/*
모든함수.__proto__ === Function.prototype
모든함수.prototype.constructor ===  함수자신
모든함수.constructor === 부모객체
*/

 

Constructor란?

  • Constructor는 어떠한 객체를 생성하는 '생성자 함수'이며 이름은 대문자로 시작합니다. 생성자 함수도 객체이므로 __proto__ 프로퍼티를 가지고 있습니다. 함수의 프로퍼티들은 console.dir(함수 이름)으로 확인할 수 있습니다.

  • 생성자를 사용하면 이름이 같은 메서드와 프로퍼티를 가진 객체를 효율적으로 여러 개 생성할 수 있습니다. 이때 메서드들 생성자의 프로토타입 객체에 추가해 두면 메모리 낭비를 피할 수 있으며 그 메서드를 다른 생성자에게 상속할 수 있습니다. 

  • 생성자 함수로 만들어낸 객체는 Instance를 흉내 냅니다. Instance를 흉내 낸 객체는 함수가 아니므로 prototype이 없습니다.

 

ES6 Class 문법

ES6의 Class는 기존의 prototype 기반의 상속을 보다 명료하게 사용할 수 있도록 해줍니다. 하지만 이 Class는 다른 객체지향 언어에서 사용되는 Class 문법과는 다릅니다. Class 문법은 새로운 객체지향 상속 모델을 제공하는 것은 아니며, 다만 기존의 Prototype을 기반으로 상속을 흉내 내도록 구현해 사용합니다. 코드로 비교해 보겠습니다.

function Person(name, first, second) {
  this.name = name;
  this.first = first;
  this.second = second;
}

Person.prototype.sum = function() {
  return this.first + this.second;
}

 위의 생성자 코드를 클래스 구문으로 만들면 아래와 같습니다.

class Person{
  constructor(name, first, second){  //생성자를 사용한 초기화
    this.name = name;
    this.first = first;
    this.second = second;
   }
   
  sum() {  //prototype 메서드
    return this.first + this.second;
  }
} 

 

클래스 선언문으로 정의한 생성자는 함수 선언문으로 정의한 생성자와 같습니다. 하지만 차이점도 있습니다.

  • 함수 선언과 달리 클래스 선언은 letm const 키워드로 선언한 변수와 같이, 호이스팅이 일어나지 않기 때문에 클래스를 사용하기 위해서는 먼저 선언을 해줍니다. 

  • 클래스 선언문은 한 번만 작성할 수 있습니다. 같은 이름을 가진 클래스 선언문을 두 번 이상 작성하면 타입 오류가 발생합니다.

  • 클래스 선언문에 정의한 생성자만을 따로 호출할 수 없습니다.

  • this에 추가한 프로퍼티를 class field라고 부릅니다.

  • constructor는 new 연산자 없이 호출할 수 없습니다.(TypeError발생)

위의 생성자는 클래스 표현식으로도 정의할 수 있습니다.

let Person = class {
  constructor(name, first, second) {
    this.name = name;
    this.first = first;
    this.second = second;
  }
  
  sum() {
    return this.first + this.second;
  }
}

Class 구문에서는 생성자의 메서드를 정적 메서드 라고 부릅니다. 정적 메서드는 prototype에 연결되지 않고 클래스에 직접 연결되기 때문에 Class의 인스턴스 없이 호출이 가능하며 Class가 인스턴스화 되면 호출할 수 없습니다. 정적 메서드를 정의하려면 클래스 멤버 앞에 static키워드를 붙여줍니다. 정적 메서드는 종종 어플리케이션의 유틸리티 함수를 만드는 데 사용됩니다. 코드로 보겠습니다.

class Person{
  constructor(name, first, second){  
    this.name = name;
    this.first = first;
    this.second = second;
    Person.personCount++;
   }
   
   sum() {
    return this.first + this.second;
  }
  
   static count() {  //정적 메서드
    return Person.personCount;
  }
} 

Person.personCount = 0

let kim = new Person('kim', 10, 20);
console.log(Person.count());  // 1

let lee = new Person('lee', 15, 30);
console.log(Person.count());  //2

console.log(kim.personCount());  //Uncaught TypeError 타입애러가 발생합니다

Class Inheritance :  클래스 상속

클래스에 extends 키워드를 붙여주면 다른 생성자를 상속받을 수 있습니다. 그리고 그 생성자에 새로운 메서드를 추가해서 확장할 수 있습니다. Person이라는 클래스에 Student라는 자식 클래스를 만들어보겠습니다.

class Student extends Person {
  constructor(name, first, second, third, grade) {
    super(name, first, second);
    this.third = third;
    this.grade = grade;
  }
  
  sayHello() {
    console.log(`Hello, I'm a student and my grade is ${this.grade}.`);
  }
  
  sum() {
    return this.first + this.second + this.third;
  }
}

let choi = new Student('choi', 10, 30, 6);

console.log(choi.name);  //choi
console.log(choi.sum());  //46
console.log(choi.sayHello());  //Hello, I'm a student and my grade is 6.
  • super 키워드를 사용해서 슈퍼 클래스의 메서드를 호출할 수 있습니다. 서브 클래스와 슈퍼 클래스에 같은 이름의 메서드가 존대하면 슈퍼 클래스의 메서드는 호출되지 않습니다.

  • 위 예제에서 서브타입의 생성자 Student는 슈퍼타입 생성자의 메서드 sum()을 덮어쓰고 있습니다. 이를 Overriding이라고 합니다.

레퍼런스:

https://medium.com/better-programming/prototypes-in-javascript-5bba2990e04b

https://poiemaweb.com/js-object-oriented-programming

https://poiemaweb.com/es6-class