- Published on
2024.05.22
[딥다이브] - 19. 프로토타입(1)
자바스크립트를 이루고 있는 거의 모든 것은 객체다. 자바스크립트는 클래스 기반 객체지향 프로그래밍 언어보다 효율적이고 강력한 객체지향 프로그래밍 능력을 가진 프로토타입 기반의 객체지향 프로그래밍 언어다.
클래스
ES6에서 도입된 클래스는 생성자 함수보다 엄격하며 생성자 함수에서 제공하지 않는 기능도 제공한다. 클래스는 프로토타입 기반 객체 생성 패턴의 Syntactic sugar
로 보기보다는 새로운 객체 생성 메커니즘으로 보는 것이 합당하다.
19.1. 객제지향 프로그래밍
객체지향 프로그래밍 : 프로그램을 명령어 또는 함수의 목록으로 보는 명령형 프로그래밍의 절차지향적 관점에서 벗어나 객체의 직합으로 프로그램을 표현하려는 프로그래밍 패러다임
추상화 : 다양한 속성 중에서 프로그램에 필요한 속성만 간추려 표현하는 것
'name'과 'page'라는 속성을 갖는 book이라는 객체를 자바스크립트로 표현하면 다음과 같다.
const book = {
name: 'Deep Dive',
page: 956,
}
console.log(book) // { name: 'Deep Dive', page: 956 }
이렇게 속성을 통해 여러 개의 값을 하나의 단위로 구성한 복합적인 자료구조를 객체
라 하며, 객체지향 프로그래밍은 독립적인 객체의 집합으로 프로그램을 표현하려는 프로그래밍 패러다임이다. 객체지향 프로그래밍은 객체의 상태
를 나타내는 데이터와 상태
를 조작할 수 있는 동작
을 하나의 단위로 묶어 생각한다. 객체의 상태
데이터를 프로퍼티
, 동작
을 메서드
라 한다.
19.2. 상속과 프로토타입
상속
은 객체지향 프로그래밍의 핵심 개념으로, 어떤 객체의 프로퍼티
또는 메서드
를 다른 객체가 상속받아 그대로 사용할 수 있는 것을 말한다. 동일한 생성자 함수에 의해 생성된 모든 인스턴스가 동일한 메서드를 중복 소유하는 것은 메모리를 불필요하게 낭비한다. 자바스크립트는 프로토타입
을 기반으로 상속
을 구현하는데, 상속
을 통해 코드를 재사용하고 중복을 제거할 수 있다. 공통적으로 사용할 프로퍼티나 메서드를 프로토타입에 미리 구현해 두면 생성자 함수가 생성할 모든 인스턴스는 별도의 구현 없이 상위(부모) 객체인 프로토타입의 메서드를 상속받아 사용할 수 있다.
19.3. 프로토타입 객체
모든 객체는 [[Prototype]]
이라는 내부 슬롯을 가지며, 이 내부 슬롯의 값은 프로토타입의 참조다. 모든 객체는 하나의 프로토타입을 갖는다. 그리고 모든 프로토타입은 생성자 함수와 연결되어 있다. [[Prototype]]
내부 슬롯은 직접 접근할 수 없지만, __proto__
접근자 프로퍼티를 통해 자신의 [[Prototype]]
내부 슬롯이 가리키는 프로토타입에 간접적으로 접근할 수 있다.
__proto__
접근자 프로퍼티
19.3.1. 모든 객체는 __proto__
접근자 프로퍼티를 통해 자신의 포로토타입, 즉 [[Prototype]]
내부 슬롯에 간접적으로 접근할 수 있다 16.1절 '내부 슬롯과 내부 메서드'에서 보았듯이 내부 슬롯은 프로퍼티가 아니다. __proto__
접근자 프로퍼티를 통해 간접적으로 [[Prototype]]
내부 슬롯의 값, 프로토타입
에 접근할 수 있다.
__proto__
접근자 프로퍼티는 상속을 통해 사용된다
__proto__
접근자 프로퍼티는 객체가 직접 소유하는 프로퍼티가 아니라 Object.prototype
의 프로퍼티다. 모든 객체는 상속을 통해 Object.prototype.__proto__
접근자 프로퍼티를 사용할 수 있다.
__proto__
접근자 프로퍼티를 통해 프로토타입에 접근하는 이유
프로토타입에 접근하기 위해 접근자 프로퍼티를 쓰는 이유는 상호 참조에 의해 프로토타입 체인이 생성되는 것을 방지하기 위해서다. 프로토타입 체인은 단방향 링크드 리스트로 구현되어야 하는데 서로가 자신의 프로포타입이 되는 순환 참조 프로토타입 체인이 만들어지면 프로퍼티를 검색할 때 무한 루프에 빠진다.
__proto__
접근자 프로퍼티를 코드 내에서 직접 사용하는 것은 권장하지 않는다
코드 내에서 __proto__
접근자 프로퍼티를 직접 사용하는 것은 권장되지 않는데, 모든 객체가 __proto__
접근자 프로퍼티를 사용할 수 있는 것은 아니기 때문이다. 직접 상속을 통해 Object.prototype
을 상속받지 않는 객체를 생성할 수 있어 __proto__
접근자 프로퍼티를 사용할 수 없는 경우가 있다.
__proto__
접근자 프로퍼티 대신 프로토타입의 참조를 취득하고 싶은 경우에는 Object.getPrototypeOf
메서드를 사용하고, 프로토타입을 교체하고 싶은 경우에는 Object.setPrototypeOf
메서드를 사용할 것을 권장한다.
19.3.2. 함수 객체의 prototype 프로퍼티
함수 객체만이 소유하는 Prototype 프로퍼티는 생성자 함수가 생성할 인스턴스의 프로토타입을 가리킨다. prototype
프로퍼티는 생성자 함수가 생성할 객체(인스턴스)의 프로토타입을 가리킨다. 따라서 non-constructor인 화살표 함수와 ES6 메서드 축약 표현으로 정의한 메서드는 prototype 프로퍼티를 소유하지 않으며 프로토타입도 생성하지 않는다.
모든 객체가 가지고 있는 __proto__
접근자 프로퍼티와 함수 객체만이 가지고 있는 prototype
프로퍼티는 결국 동일한 프로토타입을 가리킨다.
19.3.3. 프로토타입의 constructor 프로퍼티와 생성자 함수
모든 프로토타입은 constructor 프로퍼티를 갖는다. 이 constructor 프로퍼티는 prototype 프로퍼티로 자신을 참조하고 있는 생성자 함수를 가리킨다. 이 연결은 함수 객체가 생성될 때 이뤄진다.
// 생성자 함수
function Book(name) {
this.name = name
}
const deep = new Book('deep dive')
// deep 객체의 생성자 함수는 Book이다.
console.log(deep.constructor === Book) // true
19.4. 리터럴 표기법에 의해 생성된 객체의 생성자 함수와 프로토타입
생성자 함수에 의해 생성된 인스턴스는 프로토타입의 constructor
프로퍼티에 의해 생성자 함수와 연결된다. constructor
프로퍼티가 가리키는 생성자 함수는 인스턴스를 생성한 생성자 함수다.
하지만, 리터럴 표기법에 의한 객체 생성 방식과 같이 명시적으로 new
연산자와 함께 생성자 함수를 호출하여 인스턴스를 생성하지 않는 객체 생성 방식도 있다. (객체 리터럴, 함수 리터럴, 배열 리터럴, 정규 표현식 리터럴) 리터럴 표기법에 의해 생성된 객체도 프로토타입이 존재하지만 constructor
프로퍼티가 가리키는 생성자 함수가 반드시 객체를 생성한 생성자 함수라고 단정할 수 없다. 객체 리터럴에 의해 생성된 객체는 Object
생성자 함수가 생성한 객체가 아니다.
리터럴 표기법에 의해 생성된 객체도 상속을 위해 프로토타입이 필요하다. 따라서 리터럴 표기법에 의해 생성된 객체도 가상적인 생성자 함수를 갖는다. 프로토타입은 생성자 함수와 더불어 생성되며 prototype, constructor 프로퍼티에 의해 연결되어 있기 때문이다. 즉, 프로토타입과 생성자 함수는 단독으로 존재할 수 없고 언제나 쌍으로 존재한다.
리터럴 표기법 | 생성자 함수 | 프로토타입 |
---|---|---|
객체 리터럴 | Object | Object.prototype |
함수 리터럴 | Function | Function.prototype |
배열 리터럴 | Array | Array.prototype |
정규 표현식 리터럴 | RegExp | RegExp.prototype |
19.5. 프로토타입의 생성 시점
프로토타입은 생성자 함수가 생성되는 시점에 생성된다. 프로토타입과 생성자 함수는 항상 쌍으로 존재하기 때문이다.
19.5.1. 사용자 정의 생성자 함수와 프로토타입 생성 시점
생성자 함수로서 호출할 수 있는 함수, 즉 constructor 함수 정의가 평가되어 함수 객체를 생성하는 시점에 프로토타입도 생성된다. 반대로 생성자 함수로서 호출할 수 없는 non-constructor는 프로토타입이 생성되지 않는다. 함수 선언문은 런타임 이전에 자바스크립트 엔진에 의해 먼저 실행되는데, 이때 프로토타입도 생성된다. 빌트인 생성자 함수가 아닌 사용자 정의 생성자 함수는 자신이 평가되어 함수 객체로 생성되는 시점에 프로토타입이 생성되며, 생성된 프로토타입의 프로토타입은 언제나 Object.prototpye
이다.
19.5.2. 빌트인 생성자 함수와 프로토타입 생성 시점
빌트인 생성자 함수도 함수가 생성되는 시점에 프로토타입이 생성된다. 모든 빌트인 생성자 함수는 전역 객체가 생성되는 시점에 생성된다.
전역 객체
코드가 실행되기 이전 단계에 자바스크립트 엔진에 의해 생성되는 특수한 객체, window
, global
객체 등
객체가 생성되기 이전에 생성자 함수와 프로토타입은 이미 객체화되어 존재하며, 이후 생성자 함수나 리터럴 표기법으로 객체를 생성하면 프로토타입은 생성된 객체의 [[Prototype]]
내부 슬롯에 할당되고, 생성된 객체는 프로토타입을 상속받는다.
19.6. 객체 생성 방식과 프로토타입의 결정
객체의 생성 방법
- 객체 리터럴
- Object 생성자 함수
- 생성자 함수
- Object.create 메서드
- 클래스(ES6)
19.6.1. 객체 리터럴에 의해 생성된 객체의 프로토타입
객체 리터럴에 의해 생성되는 객체의 프로토타입은 Object.prototype이다.
19.6.2. Object 생성자 함수에 의해 생성된 객체의 프로토타입
Object 생성자 함수를 인수 없이 호출하면 빈 객체가 생성된다.
Object 생성자 함수에 의해 생성되는 객체의 프로토타입은 Object.prototype이다.
객체 리터럴과 Object 생성자 함수에 의한 객체 생성 방식의 차이는 프로퍼티를 추가하는 방식에 있는데, 객체 리터럴 방식은 객체 리터럴 내부에 프로퍼티를 추가하고, Object 생성자 함수 방식은 빈 객체를 생성한 이후 프로퍼티를 추가한다.
19.6.3. 생성자 함수에 의해 생성된 객체의 프로토타입
new 연산자와 함께 생성자 함수를 호출해 생성되는 객체의 프로토타입은 생성자 함수의 prototype 프로퍼티에 바인딩되어 있는 객체다.
프로토타입은 객체이므로 일반 객체와 같이 프로퍼티를 추가/삭제할 수 있으며 프로토타입 체인에 즉각 반영된다.
19.7. 프로토타입 체인
function Book(name) {
this.name = name
}
// 프로토타입 메서드
Book.prototype.readBook = function () {
console.log(`this book is ${this.name}`)
}
const deep = new Book('Deep Dive')
// hasOwnProperty는 Object.prototype의 메서드다.
console.log(deep.hasOwnProperty('name')) // true
deep
객체의 프로토타입은 Book.prototype
이다.
Object.getPrototypeOf(deep) === Book.prototype // true
Book.prototype
의 프로토타입은 Object.prototype
이다. 프로토타입의 프로토타입은 언제나 Object.prototype
이다.
Object.getPrototypeOf(Book.prototype) === Object.prototype // true
자바스크립트는 객체의 프로퍼티에 접근하려고 할 때 해당 객체에 접근하려는 프로퍼티가 없다면 [[Prototype]]
내부 슬롯의 참조를 따라 부모 역할의 프로토타입의 프로퍼티를 순차적으로 검색한다. 이를 프로토타입 체인이라 한다.
프로토타입 체인의 최상위에 위치하는 객체는 언제나 Object.prototype
이다. 따라서 모든 객체는 Object.prototype
을 상속받는다. Object.prototype
을 프로토타입 체인의 종점이라 하며, Object.prototype
프로토타입, 즉 [[Prototype]]
내부 슬롯의 값은 null
이다. Object.prototype
에서도 프로퍼티를 검색할 수 없는 경우 undefined
를 리턴한다.
자바스크립트 엔진은 객체 간의 상속 관계로 이루어진 프로토타입의 계층적인 구조에서 객체의 프로퍼티를 검색한다. 프로토타입 체인은 상속과 프로퍼티 검색을 위한 메커니즘이라고 할 수 있다. 이에 반해, 프로퍼티가 아닌 식별자는 스코프 체인에서 검색한다. 자바스크립트 엔진은 함수의 중첩 관계로 이루어진 스코프의 계층적 구조에서 식별자를 검색한다. 스코프 체인은 식별자 검색을 위한 메커니즘이다.
스코프 체인과 프로토타입 체인은 서로 협력하여 식별자와 프로퍼티를 검색하는 데 사용된다.