프로토타입
자바스크립트는 프로토타입 기반 객체지향 프로그래밍 언어입니다. 객체지향 프로그래밍은 상태와 동작을 하나의 논리적 단위로 구성한 복합적인 자료구조인 객체를 통해 프로그램에 필요한 속성만 추상화하여 표현하는 프로그래밍을 말합니다. 이때 핵심이 되는 개념이 “상속”입니다. 객체의 프로퍼티와 메서드를 다른 객체가 상속받아 그대로 사용할 수 있게 하는 기능이죠. 자바스크립트는 프로토타입을 활용하여 상속을 구현하고, 이를 통해 불필요한 코드와 메모리의 중복을 해결합니다.
생성자 함수 내부에서 메서드를 정의하면 생성된 인스턴스는 해당 메서드의 복사본을 개별적으로 가지게 되며, 인스턴스마다 별도의 메모리가 할당됩니다. 그러나 메서드를 생성자 함수의 프로토타입 객체에 추가할 경우, 생성된 인스턴스들은 상속된 프로토타입 객체의 메서드를 참조하여 사용하게 됩니다. 즉, 프로토타입을 활용하면 메모리 사용을 절약하고, 변경에도 유연하게 대처할 수 있게 됩니다.
프로토타입 객체(프로토타입)
프로토타입은 객체의 부모 객체의 역할을 하는 객체입니다. 모든 객체는 [[Prototype]] 이라는 내부 슬롯을 가지며, 이 내부 슬롯을 통해 프로토타입과 연결됩니다. 이때 연결되는 프로토타입은 객체의 생성 방식에 의해 결정됩니다. 객체 리터럴 방식으로 생성되었다면 프로토타입은 Object.prototype이고, 생성자 함수로 생성되었다면 프로토타입은 생성자 함수의 프로토타입(생성자 함수.prototype)입니다.
객체 간 접근
프로토타입을 설명하는 데 사용되는 객체는 3가지 종류가 있습니다. “생성자 함수”, “생성자 함수의 프로토타입”, “인스턴스 객체”. 이 3가지의 연결 관계를 정리해보겠습니다.
생성자 함수
- 자신의 prototype 프로퍼티를 통해 프로토타입 객체에 접근할 수 있습니다.
- new 연산자를 통해 인스턴스를 생성할 수 있습니다.
프로토타입(생성자 함수.prototype)
- 자신의 constructor 프로퍼티를 통해 생성자 함수에 접근할 수 있습니다.
- 인스턴스에 부모 객체로 상속됩니다.
인스턴스 객체
- __proto__ 접근자 프로퍼티를 통해 프로토타입에 간접적으로 접근할 수 있습니다.
__proto__ 접근자 프로퍼티
내부 슬롯은 객체의 숨겨진 내부 상태를 나타내며, 일반 프로퍼티가 아니기 때문에 원칙적으로 직접 접근하거나 호출할 수 없습니다. 그러나 __proto__ 접근자 프로퍼티를 사용하면 [[Prototype]] 내부 슬롯에 간접적으로 접근할 수 있습니다. __proto__는 Object.prototype의 접근자 프로퍼티이며, 모든 객체는 기본적으로 Object.prototype을 상속받기 때문에 이 프로퍼티를 사용할 수 있습니다.
프로토타입 체인은 객체의 프로토타입을 따라 상속 관계를 탐색하는 구조로, 단방향 링크드 리스트처럼 동작합니다. 이를 단방향으로 유지하는 이유는 무한 순환 참조를 방지하기 위함입니다. __proto__ 접근자 프로퍼티는 순환 참조가 감지될 경우 에러를 발생시켜 무한 루프에 빠지는 것을 방지합니다. 또한, 비교적 안전한 메서드인 Object.getPrototypeOf와 Object.setPrototypeOf 을 통해서 __proto__을 사용하지 않고도 프로토타입을 조작할 수 있습니다.
const obj = {};
const parent = { x: 1 };
Object.getPrototypeOf(obj); // obj.__proto__;
Object.setPrototypeOf(obj, parent); // obj.__proto__ = parent;
함수 객체의 prototype 프로퍼티
함수 객체의 prototype 프로퍼티는 생성자 함수로서 호출할 수 있을 때만 생성됩니다. non-constructor인 화살표 함수나 ES6 메서드 축약 표현으로 정의한 메서드에서는 prototype 프로퍼티가 생성되지 않습니다.
// 일반 함수 → 생성자 호출 가능
function RegularFunc() {}
console.log(RegularFunc.prototype); // { constructor: ƒ RegularFunc }
// 화살표 함수 → prototype 없음
const arrowFunc = () => {};
console.log(arrowFunc.prototype); // undefined
// 객체의 메서드 축약 표현 → prototype 없음
const obj = {
method() {}
};
console.log(obj.method.prototype); // undefined
리터럴 표기법에 의해 생성된 객체의 생성자 함수와 프로토타입
리터럴 표기법에 의해 생성된 객체도 상속을 위해 프로토타입 객체와 연결됩니다. 다만, 이 경우에는 Object.prototype과 연결되기 때문에 constructor 프로퍼티는 Object.prototype.constructor가 가리키는 Object 생성자 함수가 됩니다.
프로토타입의 생성 시점
생성자 함수는 자바스크립트가 기본으로 제공하는 빌트인 생성자 함수와 사용자가 직접 정의한 사용자 정의 생성자 함수로 구분할 수 있습니다.
빌트인 생성자 함수는 Object, String, Number 같은 생성자 함수를 말하며, 전역 객체가 생성될 때 빌트인 생성자 함수와 그 프로토타입 객체들이 생성됩니다. 반면, 사용자 정의 생성자 함수는 함수 정의가 평가되어 함수 객체를 생성하는 시점에 프로토타입 객체를 생성합니다. 이때, 생성자 함수의 프로토타입도 객체이므로 [[Prototype]] 내부 슬롯이 존재하며, 이는 Object.prototype과 연결됩니다.
프로토타입 체인
프로토타입 체인은 자바스크립트가 객체지향 프로그래밍의 상속을 구현하는 메커니즘으로, [[Prototype]] 내부 슬롯의 참조를 따라 자신의 부모 역할을 하는 프로토타입의 프로퍼티를 순차적으로 검색하는 것을 의미합니다.
프로토타입 체인의 최상위에 위치하는 객체는 언제나 Object.prototype이며, 이를 프로토타입 체인의 종점(end of prototype chain)이라고 합니다. 이때, Object.prototype의 프로토타입, 즉 [[Prototype]] 내부 슬롯의 값은 null입니다. 프로토타입 체인을 따라 프로퍼티를 검색할 때, 체인의 종점에도 없다면 에러가 아니라 undefined를 반환합니다.
오버라이딩과 프로퍼티 섀도잉
프로토타입 체인을 따라 프로퍼티나 메서드를 탐색할 때, 동일한 이름이 있을 경우 프로토타입 체인에서 순서가 빠른 자식 객체의 속성을 우선적으로 적용합니다. 오버라이딩(overriding)은 메서드를 덮어쓰는 경우, 프로퍼티 섀도잉(property shadowing)은 값을 덮어쓰는 경우를 말합니다.
정적 프로퍼티/메서드
정적(static) 프로퍼티/메서드는 생성자 함수로 인스턴스를 생성하지 않아도 참조/호출할 수 있는 프로퍼티/메서드를 말합니다. 정적 프로퍼티/메서드는 프로토타입 체인에 속하지 않기 때문에 인스턴스에서 접근할 수 없습니다.
프로퍼티 존재 확인
프로퍼티를 확인하는 2가지 방법 중 in 연산자는 상속받은 모든 프로토타입의 프로퍼티를 확인하고, Object.prototype.hasOwnProperty 메서드는 객체 자신의 고유 프로퍼티만 확인합니다.
프로퍼티 열거
for…in 문
for...in 문은 객체의 프로토타입 체인 상에 존재하는 모든 프로토타입의 프로퍼티 중에서 프로퍼티 어트리 뷰트 [[Enumerable]]의 값이 true인 프로퍼티를 순회하며 열거enumeration합니다.
Object.keys/values/entries 메서드
Object.keys/values/entries 메서드는 열거 가능한(enumerable) 객체 자신의 고유 프로퍼티만 열거합니다. Object.keys 메서드는 프로퍼티 키를 배열로 반환하고, Object.values 메서드는 프로퍼티 값을 배열로 반환하고, Object.entries 메서드는 프로퍼티 키와 값의 쌍의 배열을 배열에 담아 반환합니다.