Javascript 프로토타입


자바스크립트에 대한 설명으로 프로토타입 기반(prototype based) 프로그래밍 언어라는 표현을 종종 볼 수 있습니다. 이번 글에서는 그 프로토타입에 대해서 다루려고 합니다.

프로토타입이란?

프로토타입은 자바스크립트에서 객체 지향 프로그래밍, 그 중에서도 특히 상속을 구현하기 위한 메커니즘 입니다. 모든 객체는 프로토타입을 프로퍼티로 갖고 있습니다. 표기할 때는 주로 object.[[Protoype]]을 사용하는데 대부분의 브라우저에서는 __proto__ 식별자로 객체의 프로토타입에 접근할 수 있습니다. 하지만 ES6에서는 __proto__의 사용은 지양하고 Object.getPrototypeOf() 또는 Object.setPrototypeOf()를 사용하는 것을 권장합니다.

프로토타입 체인

자바스크립트에서 객체의 프로퍼티에 접근하면 제일 먼저 해당 객체의 프로퍼티를 검색합니다. 그리고 해당 프로퍼티가 있으면 해당 객체의 프로퍼티 값을 반환합니다. 하지만 객체에 해당 프로퍼티가 없다면 객체의 프로토타입에서 프로퍼티를 찾습니다. 그리고 이 과정을 __proto__null이 될 때까지 반복하고 그 때까지 프로퍼티를 찾지 못했다면 undefined를 반환합니다. 이런식으로 프로토타입으로 연결된 객체 구조를 프로토타입 체인이라고 합니다.

const o1 = {
    a: 1,
    b: 2,
};

console.log(o1);

/*	콘솔
a: 1
b: 2
[[Prototype]]: Object
*/

코드를 실행하면 선언할 때는 없던 [[Prototype]] 프로퍼티가 생긴 것을 확인할 수 있습니다. 이는 자바스크립트가 객체를 생성할 때 객체의 프로토타입을 전역 Object로 설정하기 때문입니다. 전역 Object에는 hasOwnProperty()와 같이 모든 객체에서 사용할 수 있는 메소드들이 정의되어 있기 때문에 지금까지 아무렇지 않게 모든 객체에 대해 해당 메서드들을 사용할 수 있었던 것입니다. 그리고 전역 Object의 [[Prototype]]은 null입니다. 따라서 직접 __proto__프로퍼티를 수정하지 않는 이상 모든 프로토타입 체인의 꼭대기에는 전역 Object가 있습니다.

이번에는 o1을 프로토타입으로 하는 o2를 만들어봅시다. 코드는 아래와 같습니다.

const o1 = {
  a: 1,
  b: 2,
};

const o2 = Object.create(o1);
o2.c = 3;

console.log(o2);

/*	콘솔
c: 3
[[Prototype]]: Object
    a: 1
    b: 2
    [[Prototype]]: Object
*/

코드를 실행하면 o2의 프로토타입이 o1인 것을 확인할 수 있습니다. o2의 프로토타입 체인에 o1이 있으므로 o2.a와 같은 코드는 에러를 발생시키지 않습니다.

생성자 함수의 프로토타입

함수 또한 Object를 프로로타입으로 하는 객체이기 때문에 __proto__프로퍼티가 존재합니다. 하지만 함수에는 그 외에 prototype이라는 특수한 프로퍼티가 있습니다. 이는 함수를 생성자로 사용했을 때 생성되는 객체의 프로토타입을 가리킵니다. 이것을 활용해 OOP에서의 메서드를 구현할 수 있습니다.

예를 들어 Student 함수가 있고 모든 Student의 인스턴스에 goToSchool()함수를 추가하고 싶다고 해봅시다. 그런 경우에 아래와 같이 코드를 작성할 수 있습니다.

function Student(name) {
    this.name = name;
    this.goToSchool = function() {
        console.log(`${this.name} went to school!`);
    }
}
const student1 = new Student('John Doe');
const student2 = new Student('Alex');

student1.goToSchool();	// John Doe went to school!
student2.goToSchool();	// Alex went to school!

위 코드는 우리가 원했던 대로 동작은 합니다. 하지만 위 방법은 새로운 객체를 생성할 때마다 새로운 goToSchool() 메서드를 생성해준다는 문제가 있습니다. 실제로 console.log(student1.goToSchool == student2.goToSchool);를 실행해보면 false가 출력되는 것을 확인할 수 있습니다. 이를 피하기 위해서 있는 것이 함수의 prototype 프로퍼티입니다. 위 코드를 아래와 같이 수정해봅시다.

function Student(name) {
  this.name = name;
}

Student.prototype.goToSchool = function () {
  console.log(`${this.name} went to school!`);
};

const student1 = new Student('John Doe');
const student2 = new Student('Alex');

student1.goToSchool();	// John Doe went to school!
student2.goToSchool();	// Alex went to school!

console.log(student1.goToSchool == student2.goToSchool);	// true

이제는 동작은 똑같이 하지만 모든 Student의 객체가 하나의 goToSchool메서드를 참조하기 때문에 메모리상에서 이득이 있습니다. 하지만 ES6 에서 추가된 class 키워드를 사용하면 훨씬 가독성도 좋은 방법으로 위와 동일한 코드를 작성할 수 있습니다. 아래는 class를 이용한 코드입니다.

class Student {
  constructor(name) {
    this.name = name;
  }

  goToSchool() {
    console.log(`${this.name} went to school!`);
  }
}

const student1 = new Student('John Doe');
const student2 = new Student('Alex');

student1.goToSchool(); // John Doe went to school!
student2.goToSchool(); // Alex went to school!

console.log(student1.goToSchool == student2.goToSchool); // true

자바스크립트가 왜 프로토타입을 사용하는지에 대한 고찰을 담은 좋은 글이 있어 아래 링크를 공유합니다.

자바스크립트는 왜 프로토타입을 선택했을까