본문 바로가기

JavaScript

prototype의 명확한 이해

JavaScript는 명령형(imperative), 함수형(functional), 프로토타입 기반(prototype-based), 객체지향 프로그래밍(OOP)을 지원하는 멀티 패러다임 프로그래밍 언어이다.

 

prototype-based는 어떤 의미일까?

클래스 기반언어에서는 상속을 사용한다. 상속은 객체지향 프로그래밍의 핵심 개념으로, 어떤 객체의 프로퍼티 또는 메서드를 다른 객체가 그대로 사용할 수 있는 것을 말한다. 하지만, 프로토타입 기반 언어에서는 어떤 객체를 '원형'으로 삼고 이를 복제함으로써 상속과 비슷한 효과를 얻는다.

 

JavaScript는 프로토타입을 기반으로 상속을 구현하여 불필요한 중복을 제거한다. 중복을 제거하는 방법은 기존의 코드를 재사용하는 것이다. 아래 코드를 보자.

 

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)

console.log(circle.getArea === circle2.getArea) // false

 

위 코드에서 JavaScript 생성자 함수를 이용해서 인스턴스를 만들었고, getArea라는 함수가 같은지 비교했다. 위 코드는 생성자 함수의 '문제'를 보여준다.

 

우리는 지금 객체지향 프로그래밍을 바탕으로 중복을 줄이고 싶은데, 인스턴스를 만들 때 같은 함수를 중복해서 만드는 상황이 발생한 것이다. 이는 메모리의 불필요한 낭비로 이어진다. 또한 인스턴스를 생성할 때마다 메서드를 생성하므로 퍼포먼스에도 악영향을 준다. 따라서 이러한 메모리의 낭비를 방지하기 위해 탄생한 것이 프로토타입이다. (ES2015 부터는 class를 공식적으로 지원함에 따라 프로토타입을 사용하지 않아도 프로토타입과 동일한 효과를 낼 수 있게 되었다. 그러나 문법적인 양념일 뿐이며 자바스크립트는 여전히 프로토타입 기반의 언어이다.)

그렇다면 바람직한 상황은 무엇일까? getArea를 공유하는 것이다!

 

JavaScript는 prototype을 기반으로 상속을 구현한다.

 

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

Cicle.prototype.getArea = function() {
    return Math.PI * this.radius ** 2
}

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

console.log(circle1.getArea === circle2.getArea) // true

 

상속에 의한 메서드 공유

 

Circle 생성자 함수가 생성한 모든 인스턴스는 자신의 프로토타입, 즉 상위(부모) 객체 역할을 하는 Circle.prototype의 모든 프로퍼티와 메서드를 상속받는다. 메서드를 생성자 밖으로 꺼냄으로써 객체가 생성될 때마다 반복적으로 생성하는 시간과 반복적 생성으로 인한 메모리 낭비를 최소화할 수 있다.

 

상속은 코드의 재사용이라는 관점에서 매우 유용하다. 생성자 함수가 생성할 모든 인스턴스가 공통적으로 사용할 프로퍼티나 메서드를 프로토타입에 미리 구현해 두면 생성자 함수가 생성할 모든 인스턴스는 별도의 구현없이 상위(부모) 객체인 프로토타입의 자산을 공유하여 사용할 수 있다.

 

prototype 객체는 객체지향 프로그래밍의 근간을 이루는 객체 간 상속을 구현하기 위해 사용된다. 프로토타입은 어떤 객체의 상위 객체의 역할을 하는 객체로서 다른 객체에 공유 프로퍼티(메서드 포함)을 제공한다. 프로토타입을 상속받는 자식 객체에서는 상위 객체의 프로퍼티를 자신의 프로퍼티처럼 자유롭게 사용할 수 있다. 

 

모든 객체는 [[Prototype]]이라는 내부 슬롯을 가지며, 이 내부 슬롯의 값은 프로토타입의 참조(null인 경우도 있다.)다. [[Prototype]]에 저장되는 프로토타입은 객체 생성 방식에 의해 결정된다. 즉, 객체가 생성될 때 객체 생성 방식에 따라 프로토타입이 결정되고 [[Prototype]]에 저장된다.

 

예를 들어, 객체 리터럴에 의해 생성된 객체의 프로토타입은 Object.prototype이고 생성자 함수에 의해 생성된 객체는 프로토타입은 생성자 함수의 prototype 프로퍼티에 바인딩되어 있는 객체이다. 모든 객체는 하나의 프로토타입을 갖는다. 그리고 모든 프로토타입은 생성자함수와 연결되어있다. 즉 객체와 프로토타입 생성자 함수는 다음그림과 같이 서로 연결되어있다. 

 

객체와 프로토타입과 생성자 함수는 서로 연결되어 있다

[[Prototype]] 내부 슬롯에는 직접 접근할 수 없지만, 위 그림처럼 __proto__ 접근자 프로퍼티를 통해 자신의 프로토타입, 즉 자신의 [[Prototype]] 내부 슬롯이 가리키는 프로토타입에 간접적으로 접근할 수 있다. 그리고 프로토타입은 자신의 constructor 프로퍼티를 통해 생성자 함수에 접근할 수 있고, 생성자 함수는 자신의 prototype 프로퍼티를 통해서 프로토타입에 접근할 수 있다.

 

지금까지의 흐름을 정리해보자. 어떤 생성자 함수를 new 연산자와 함께 호출한다면 생성자 함수에서 정의된 내용을 바탕으로 새로운 인스턴스가 생성된다. 이 인스턴스에는 __proto__라는 프로퍼티가 자동으로 부여되고, 이 프로퍼티는 생성자함수의 prototype을 참조한다. 

 

prototype과 __proto__는 객체이다. prototype 객체 내부에서는 인스턴스가 사용할 메서드를 저장한다.(코드 재사용) 인스턴스에서는 숨겨진 프로퍼티인 __proto__를 통해서 prototype의 메서드에 접근할 수 있다. 

 

ECMAScript 명세에는 __proto__가 아니라 [[Prototype]]이라는 명칭으로 정의돼 있고 __proto__는 브라우저들이 [[Prototype]]을 구현한 대상이다. 또한, 명세에는 instance.__proto__와 같은 방식으로 접근하는 것을 허용하지 않고 오직Object.getPrototypeOf(instance) / Reflect.getPrototypeOf(instance)를 통해서만 접근할 수 있도록 정의한다.

하지만 이런 명세에도 불구하고 대부분의 브라우저들이 __proto__에 직접 접근하는 방식을 포기하지 않았고 결국 ES6에서는 이를 브라우저에서 동작하는 레거시 코드에 대한 호환성 유지 차원에서 정식으로 인정기에 이르렀다.

 

__proto__

__proto__는 접근자 프로퍼티이다. JavaScript는 원칙적으로 내부 슬롯과 내부 메서드에 직접적으로 접근하거나 호출할 수 있는 방법을 제공하지 않는다. 하지만 [[Prototype]]의 경우 내부슬롯에 __proto__접근자 프로퍼티를 통해 간접적으로 [[Prototype]]의 내부 슬롯의 값, 즉 프로토타입에 접근 가능하다.

 

 

 

 

참고

Modern JavaScript Deep Dive

Core JavaScript

'JavaScript' 카테고리의 다른 글

자바스크립트 완벽 가이드 | 객체  (0) 2022.06.30
closure  (0) 2022.06.02
함수와 일급 객체  (0) 2022.05.25
[JavaScript] Nullish Coalescing  (0) 2022.05.19
[JavaScript] new.target  (0) 2022.05.12