파이썬, Java, C++는 모두 클래스를 선언하여 객체를 생성하고,
상속을 구현하는 클래스 기반 객체지향 프로그래밍 언어입니다.
자바스크립트 역시 객체지향 프로그래밍 언어이지만,
위 언어들과는 객체와 상속을 다루는 방식에서 차이점이 있습니다.
이번 글에서는 자바스크립트의 프로토타입(prototype)에 대해 알아보면서,
자바스크립트는 프로토타입 기반의 객체지향 프로그래밍 언어이다.
라는 말의 의미를 정확히 이해해보겠습니다.
목차
-
- 왜 프로토타입 기반 객체 생성 방식일까?
- 클래스 기반 언어?
- 생성자 함수와 프로토타입 예시
- 프로토타입 객체(prototype, __proto__)
- 프로토타입이란
- '생성자 함수, 프로토타입, 객체' 의 구조
- 함수 객체 생성 시 내부 동작
- 프로토타입 체인 vs 스코프 체인
- 프로토타입 교체
- 왜 프로토타입 기반 객체 생성 방식일까?
요약
→ 다른 객체지향 언어들이 클래스를 기반으로 상속을 구현하는 것과 달리, 자바스크립트는 프로토타입을 기반으로 상속을 구현한다. (*ES6에서 클래스 도입) ⇒ 중복 제거 (코드 재사용) 가능.
모든 객체는 [[Prototype]]이라는 내부 슬롯을 가지며, 이는 프로토타입의 참조를 가리킨다. 프로토타입은 객체 간 상속을 구현하는 메커니즘으로, 프로퍼티와 메서드를 객체 간 공유할 수 있게 한다.
모든 객체는 proto 접근자 프로퍼티를 통해 자신의 프로토타입, 즉 [[Prototype]] 내부 슬롯에 간접적으로 접근할 수 있다. Object.prototype은 모든 객체의 원형이며, 프로토타입 체인은 프로토타입이 단방향 링크드 리스트 형태로 연결되어 있는 상속 구조를 말한다.
1. 왜 프로토타입 기반 객체 생성 방식일까?
프로그래밍 언어는 크게 명령형(절차지향형)과 객체지향형으로 나눌 수 있습니다.
- 명령형(절차지향형) : 프로그램은 명령어 또는 함수의 목록임.
- 객체지향형 : 프로그램은 독립적 단위, 즉 객체의 집합임.
객체지향에서 핵심 개념 중 하나는 바로 추상화입니다.
이는 객체가 가질 수 있는 다양한 속성 중에서 필요한 속성만을 간추려 설계하는 것을 말합니다.
클래스 기반 언어?
자바스크립트는 초기에 브라우저 내에서 간단한 상호작용을 위해 만들어진 스크립팅 언어로,
함수와 객체 리터럴을 중심으로 동작하도록 설계되었습니다.
이 때문에 자바스크립트는 전통적인 클래스 기반 언어와는 다른 객체 모델을 가지고 있지만,
ES6부터는 class 키워드를 도입해 클래스 문법도 지원하게 되었습니다.
그럼에도 불구하고, 겉으로는 클래스처럼 보이는 문법도
내부적으로는 여전히 프로토타입(prototype) 기반으로 동작합니다.
(즉, class와 extends 같은 최신 문법은 모두
프로토타입 체인을 이용한 상속과 메서드 공유의 문법적 설탕(syntactic sugar)에 불과합니다.)
생성자 함수와 프로토타입 예시
먼저 생성자 함수를 이용한 객체 생성 예제를 살펴봅시다.
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);
이렇게 작성하면 circle1과 circle2는 각각 자신만의 getArea 메서드를 가지게 됩니다.
즉, 코드 중복은 없지만, 메모리가 중복으로 사용되는 문제가 발생합니다.
이를 개선하려면 공통 메서드를 프로토타입에 정의해주는 방식이 효과적입니다.
function Circle(radius) {
this.radius = radius;
}
Circle.prototype.getArea = function() {
return Math.PI * this.radius ** 2;
};
const circle1 = new Circle(1);
const circle2 = new Circle(2);
이번엔 getArea 메서드가 Circle.prototype에 정의되었기 때문에,
모든 인스턴스는 하나의 메서드를 공유하게 됩니다.
이를 통해 내부적으로는 메모리 낭비를 방지하고,
객체 간 메서드 일관성을 확보할 수 있습니다.
2. 프로토타입 객체 (prototype, __proto__)
프로토타입이란
프로토타입은 "어떤 객체의 부모 역할을 하는 객체"입니다.
자바스크립트 엔진 내부에는 객체가 가지는 숨겨진 정보·동작을 담은 내부 슬롯과 내부 메서드가 있고,
이들은 모두 [[…]] 형태로 표기됩니다.
그중 [[Prototype]] 내부 슬롯이 바로 프로토타입 참조를 저장합니다.
function Person(name) {
this.name = name;
}
// 생성자 함수에 붙는 prototype
console.log(Person.hasOwnProperty('prototype')); // true
const p = new Person('Lee');
// 인스턴스가 상속받는 __proto__
console.log(p.hasOwnProperty('__proto__')); // false
console.log(p.__proto__ === Person.prototype); // true
구분 | 설명 |
prototype | · “생성자 함수”가 자동으로 갖는 데이터 프로퍼티· 새 인스턴스의 [[Prototype]]을 가리킴 |
__proto__ | · 모든 인스턴스가 상속받는 접근자 프로퍼티(getter/setter)· 자신의 [[Prototype]]에 간접 접근 |
❗ 잠깐정리
prototype을 갖는 함수 == 생성자가 될 수 있는 함수( Constructible )
1. Constructible 함수
1) 함수 선언문 : function f(){}
2) 함수 표현식 : const f = function(){}
2. Non-constructible 함수
1) 화살표 함수 : () => {}
2) bound함수 : Function.prototype.bind
3) 클래스 메서드로 정의된 함수
'생성자 함수, 프로토타입, 객체' 의 구조
모든 프로토타입 객체는 constructor 프로퍼티를 가집니다.
constructor는 다시 자기 자신을 생성한 생성자 함수를 가리키므로,
객체 ⇄ 프로토타입 ⇄ 생성자 함수가 서로 연결되어 있는 구조가 만들어집니다.
함수 객체 생성 시 내부 동작
함수 객체가 생성될 때 내부적으로 일어나는 일을 정리해보면 다음과 같습니다.
- 함수 선언문은 런타임 이전에 자바스크립트 엔진에 의해 먼저 실행된다. (함수 호이스팅)
- 해당 함수 객체에 prototype 데이터 프로퍼티를 자동 생성한다.
- 이 prototype안에 constructor 프로퍼티가 자동으로 생성되어,
다시 원래의 생성자 함수를 가리키도록 연결된다. - 이후 new로 인스턴스를 생성하면, 인스턴스의 [[Prototype]](즉 __proto__)가
이 함수의 prototype 객체를 참조하도록 설정된다. - prototype의 속성들은 상속 받아 인스턴스(객체)에서 사용할 수 있다.
- 때문에 인스턴스는 constructor 프로퍼티를 상속받아 사용할 수 있다.
- constructor 프로퍼티를 통해 생성자 함수와 인스턴스가 연결된다.
결과적으로 생성자 함수 → 프로토타입 객체 → 인스턴스로 이어지는
단방향 링크드 리스트,
즉 프로토타입 체인이 완성됩니다.
(*체인 최상단은 null값 입니다.)
프로토타입 체인 vs 스코프 체인
- 프로토타입 체인: 객체 간 상속과 프로퍼티 검색을 위한 메커니즘
- 스코프 체인: 식별자(변수·함수명) 검색을 위한 메커니즘
프로토타입 교체
객체 간의 상속 관계는 프로토타입 수정을 통해 동적으로 변경할 수 있습니다.
하지만, 프로토타입을 직접 교체 시
constructor 프로퍼티와 생성자 함수 간의 연결이 파괴됩니다.
때문에 프로토타입 교체를 동적으로 변경하는 것은 지양해야 하며,
상속 관계를 인위적으로 설정하려면 다음과 같은
“직접 상속” 방식을 사용하는 편이 더 안전하고 명확합니다.
const child = Object.create(parentProto);
참고자료
- 모던 자바스크립트 Deep Dive (19장. 프로토타입, 16장. 프로퍼티 어트리뷰트)