프로토 타입이란?
Prototype이란, '원형' 의 뜻을 가지며, 시스템의 미완성 버전 또는 중요한 가능들이 포함된 시스템의 초기모델을 의미한다.
JS에서는 모든 것이 객체입니다. 우리가 사용하는 배열이나 함수도 하나의 객체인데요. 우리가 정의하지 않았던 JS의 내장 함수들, 배열의 forEach(), includes()와 함수의 bind()와 같은 함수들은 우리가 정의하지 않아도 객체에 상속되어 사용할 수 있습니다. 이를 위해 JS는 Prototype, 즉 '객체의 원형' 을 정의하는 방식을 통해 객체가 가지는 특성이나 메소드의 상속을 구현합니다. Prototype을 이해하기 위해서는 반드시 '객체의 원형' 이라는 의미를 기억하는 것이 중요합니다. 이번 포스트에서는 JS가 Prototype을 통해 어떻게 상속을 구현할 수 있는지를 알아보고자 합니다. 본론으로 들어가기 전에 알아야 할 것이 하나 있습니다.
생성자 함수와 new 연산자를 통한 객체 생성과정
객체의 상속을 구현하기 전에 우리는 객체의 생성방법에 대해서 이해해야 합니다. 객체 생성과정에서 Prototype을 사용하기 때문입니다. 생성자 함수는 함수 내에서 this를 통해서 새롭게 생성할 객체의 형식을 정의하는 함수입니다. 생성자 함수로 객체의 형식을 정의했다면 new 연산자를 통해서 새로운 객체를 생성합니다.(여기서 생성자 함수는 일반적인 함수와 다를 것이 없지만, 클래스와 같이 함수명의 첫 글자를 대문자로 사용하는 관례가 존재합니다.) 객체 생성과정을 좀 더 세분화하면 아래의 순서를 가집니다.
-
새로운 객체를 생성하고 생성된 객체가 생성자 함수의 Prototype Object를 참조할 수 있도록 합니다.
구체적으로 표현하자면, 생성자 함수가 가지는 Prototype Object를 Prototype Link에 바인딩 된 새로운 객체를 생성합니다.
- 생성된 객체를 함수 내의
this로 바인딩하여 함수를 수행합니다. 이때,this를 통해 명시적으로 호출되는 프로퍼티와 메소드를 객체에 추가합니다.
- 함수의 수행이 완료되면 해당 객체를 반환합니다.
여기서 우리가 집중할 것은 첫 번째 과정입니다. Prototype이라는 단어가 가지는 '원형'이라는 의미에서처럼 JS는 객체에 필요한 프로퍼티와 메소드를 전달하기 위해 Prototype을 바인딩하는 작업이 가장 먼저 수행되고 있습니다. 그럼 이제 Prototype을 구성하는 두 개념 Prototype Object와 Prototype Link에 대해 알아보도록 하겠습니다. (두 번째 과정의 this는 다음 포스팅에서 설명하도록 하겠습니다.)
프로토타입의 구조
Prototype은 객체 자기 자신의 원형을 가리키는 Prototype Link, 자신을 통해 생성될 객체의 원형을 의미하는 Prototype Object로 구성되어있습니다. 이 두 가지 개념을 하나씩 살펴보겠습니다.
Prototype Object
함수가 가지는 프로퍼티로, 자신(함수)을 통해 생성되는 객체들의 원형 객체를 말한다. (자신의 원형이 아님, 이후에 생성될 객체들의 원형인 것을 주의하자!)
중요한 것은 이 객체가 '함수가 가지는 프로퍼티'라는 것입니다. Prototype Object는 생성자 함수를 통해 생성될 객체의 원형을 의미합니다. 객체를 생성할 수 있는 함수에서 사용되는 객체임을 기억해야 합니다. 위에서 생성자 함수를 통한 객체 생성 과정을 설명하였습니다. 그 과정에서 Prototype Object는 함수가 선언되는 시점에 함수와 함께 생성되고, 생성된 Prototype Object는 함수의 prototype 프로퍼티에 바인딩 됩니다.
function Human(name) {
this.name = name;
} 위의 예제에서 이름 프로퍼티와 인스턴스의 이름을 출력하는 메소드를 가지는 객체의 생성자 함수 Human을 선언하였습니다. JS는 생성자 함수 Human를 선언하는 시점에 자신의 Prototype Object를 생성하고 prototype프로퍼티에 바인딩합니다. 아래는 결과를 그림으로 표현한 것입니다.
1. 아래 박스는 생성자 함수 Human입니다. 생성자 함수는 기본적으로 두 개의 프로퍼티를 가집니다.
-
prototype프로퍼티생성자 함수는 자신을 통해 생성될 객체의 원형, Prototype Object를
prototype프로퍼티를 통해 참조하고 있습니다.
-
[[Prototype]]프로퍼티아래에서 설명할
Prototype Link를 의미하므로 추후 설명하도록 하겠습니다.
2. 위 박스는 생성자 함수 Human의 Prototype Object입니다.
객체의 원형이라는 의미는 곧 Human 함수를 통해 생성될 객체들이 사용할 수 있는 영역이라고 생각하면 되겠습니다. 모든 함수의 Prototype Object는 기본적으로 그림과 같이 두 개의 프로퍼티와 빈 슬롯{}을 가집니다. 구조를 하나씩 살펴보겠습니다.
-
constructor프로퍼티Prototype Object이 가지는 프로퍼티로, 자신을 사용하는 생성자 함수를 가리킨다.
해당 프로퍼티를 통해서 객체는 자신의 생성자 함수를 참조할 수 있습니다. 주로 생성자 함수가 가지는 static method를 사용하고자 할 때 사용하거나, 해당 객체와 동일한 생성자 함수를 통한 객체를 만들기 위함입니다.
-
[[Prototype]]프로퍼티모든 객체가 가지는 프로퍼티로, 객체 자신의 원형 객체인 Prototype Object를 가리킵니다.
가장 헷갈리는 부분 중의 하나가 Prototype Object의
[[Prototype]]프로퍼티입니다. Prototype Object도 하나의 객체이기 때문에[[Prototype]]프로퍼티를 가집니다. 모든 Prototype Object의[[Prototype]]프로퍼티는 동일하게Object의 Prototype Object를 가리킵니다. 즉, Prototype Object는Object를 통해서 만들어진, 말 그대로 객체라는 의미가 됩니다. (모든 객체가 Object의 자손임을 의미합니다.) 그림으로 표현하면 아래와 같습니다.
- 위 그림에서 생성자 함수
Human,Animal모두 Prototype Object를 가지며 Prototype Object들의[[Prototype]]프로퍼티는 함수가 다르더라도 동일하게Object의 Prototype Object를 가리키고 있습니다.
ObjectPrototype Object의[[Prototype]]프로퍼티는null값을 가집니다.[[Prototype]]프로퍼티가 객체 자신의 원형객체인 Prototype Object를 가리킨다고 설명하였는데ObjectPrototype Object는 자신의 원형객체가 존재하지 않으므로null이 됩니다.
- 위 그림에서 생성자 함수
-
{}Prototype을 통해 공유될 프로퍼티와 메소드가 들어갈 영역
생성자 함수의 Prototype Object이 가지는 프로퍼티와 메소드가 정의되는 영역입니다. 이후에 설명할 Prototype Link에 의해 모든 객체가 참조할 수 있으며, 생성자 함수의
prototype프로퍼티를 통해서 추가할 수 있습니다.
function Human(name) { this.name = name; } // 생성자 함수의 prototype 프로퍼티를 통해서 메소드를 추가하였다. Human.prototype.printName = function () { console.log(this.name); }
Prototype Link
모든 객체(함수 포함)가 가지는 프로퍼티로, 자신의 원형 Prototype Object을 참조하기 위해 사용되는 포인터이다.
모든 객체는 Prototype Link를 가지며, [[Prototype]] 프로퍼티를 통해서 구현됩니다. 이 프로퍼티는 자신의 원형 객체 즉, 객체 자신을 생성할 때 사용하였던 생성자 함수의 Prototype Object를 가리킵니다. 이 프로퍼티를 통해 객체는 생성자 함수의 Prototype Object가 가진 프로퍼티와 메소드를 사용할 수 있습니다. 생성자 함수 Human과 이 함수를 통해 생성되는 인스턴스 chanwoo 예제로 설명하도록 하겠습니다.
function Human(name) {
this.name = name;
}
// 생성자 함수의 prototype 프로퍼티를 통해서 메소드를 추가하였다.
Human.prototype.printName = function () {
console.log(this.name);
}
const chanwoo = new Human('chanwoo'); 인스턴스 chanwoo는 생성자 함수 Human을 사용하여 생성되었습니다. Prototype Link의 정의에 따라 chanwoo는 객체이기 때문에 [[Prototype]] 프로퍼티를 가지고 있습니다. 그림으로 표현하자면 아래와 같습니다.
[[Prototype]]프로퍼티는__proto__프로퍼티를 통해 접근할 수 있습니다. 하지만 최근에는__proto__프로퍼티 대신Object.getPrototypeOf(),Object의 정적 메소드를 사용하여 접근하도록 권장하고 있습니다.
- 인스턴스
chanwoo의[[Prototype]]프로퍼티는 객체 자신을 생성하기 위해 사용한 생성자 함수인Human의 Prototype Object를 가리키고 있습니다.
- 생성자 함수
Human의 Prototype Object의[[Prototype]]프로퍼티는Object의 Prototype Object를 가리킵니다.Object의 Prototype Object는 모든 Prototype Object의 원형이라는 의미를 가집니다.
Object의 Prototype Object의[[Prototype]]프로퍼티는null값을 가집니다. JS는 모든 것이 객체이며, 모든 객체의 원형이Object의 Prototype Object이기 때문입니다. 가장 기본이 되는 원형이기 때문에Object의 Prototype Object가 가진[[Prototype]]프로퍼티가null값을 가지게 됩니다.
그래서 Prototype Object, Prototype Link를 말하자면..
JS는 Prototype을 통해 객체 지향의 상속을 구현합니다. JS의 객체는 모두 자신의 원형이 존재합니다. 이 원형을 Prototype이라고 하는데요. Prototype은 Prototype Object와 Prototype Link로 구성되어있습니다.
- Prototype Object는 함수 측의 구현으로, 생성자 함수를 통해 생성되는 인스턴스들의 원형 객체를 말합니다. 생성자 함수가 선언되는 시점에 해당 함수의 Prototype Object가 생성되며, 함수의
prototype프로퍼티를 통해서 Prototype Object 내에 상속할 프로퍼티와 메소드를 선언할 수 있습니다.
- Prototype Link는 객체 측의 구현으로, 객체 자신이 생성될 때 사용된 원형 객체, Prototype Object를 참조하고 있습니다. Prototype Link는
[[Prototype]]프로퍼티에 저장되며, 모든 객체가 이 프로퍼티를 가지고 있습니다. 생성자 함수로 생성된 인스턴스의 경우, 함수가 가진 Prototype Object가 이 프로퍼티에 바인딩 되며, 이 프로퍼티를 통해 Prototype Object가 가진 프로퍼티와 메소드를 사용할 수 있습니다.
- 모든 Prototype Object는 인스턴스들이 참조할 수 있는 프로퍼티와 메소드가 저장될 슬롯과 두 프로퍼티를 가집니다.
constructor프로퍼티는 자신(Prototype Object)을 사용하여 인스턴스를 생성할 생성자 함수를 참조합니다.[[Prototype]]프로퍼티는 자신의 원형 객체인Object의 Prototype Object를 참조하고 있습니다. 그리고 아래에서 서술할 Prototype Chain에 의해 모든 객체는Object의 Prototype Object가 가지는 모든 프로퍼티와 메소드에 접근할 수 있습니다.
Prototype Chain
Prototype Object과 Prototype Link로 구성된 Prototype을 통해 상속을 구현하는 방식이다. 포인터를 통해 선형적으로 참조하는 형식이 chain과 같은 모양이기에 Prototype Chain이라고 합니다.
모든 객체는 자신의 원형 객체 즉, Prototype Object를 참조할 수 있습니다. 이 Prototype Object에는 객체가 상속받을 프로퍼티와 메소드가 저장되어있고, Prototype Link을 통해 Prototype Object에 저장되어있는 프로퍼티와 메소드에 접근할 수 있습니다. Prototype을 통해 상속받는 프로퍼티와 메소드를 사용하는 동작 방식은 아래와 같습니다.
- 사용하고자 하는 프로퍼티와 메소드가 객체의 슬롯
{}에 존재하는지 확인합니다.
- 현재 객체에 존재하지 않는다면, 객체의 Prototype Link를 통해 원형 객체인 Prototype Object에 접근합니다.
- 사용하고자 하는 프로퍼티와 메소드가 Prototype Object가 가진 슬롯에 존재하는지 확인합니다.
- 현재의 Prototype Object에 존재하지 않는다면, 이 Prototype Object의 객체로 간주하고 1~3번의 과정을 반복합니다.
- Prototype Object를 접근하는 과정에서 객체의 Prototype Link가
null인 경우, 즉Object의 Prototype Object인 경우 조회가 종료됩니다.
이렇게 Prototype을 통해 객체가 참조할 수 있는 프로퍼티와 메소드를 조회하는 과정에서 여러 개의 Prototype Object를 거친다고해서 Prototype Chain이라고 합니다. 위에서 사용하였던 예제와 예제의 Prototype 구조 그림을 통해 Prototype Chain을 설명해보도록 하겠습니다.
function Human(name) {
this.name = name;
}
// 생성자 함수의 prototype 프로퍼티를 통해서 메소드를 추가하였다.
Human.prototype.printName = function () {
console.log(this.name);
}
const chanwoo = new Human('chanwoo');
chanwoo.printName(); // ----- A
chanwoo.hasOwnProperty('name'); // ----- B
chanwoo.printNickName(); // ----- C 인스턴스 chanwoo는 생성자 함수가 객체의 형식을 정의하였던대로 name 프로퍼티를 가집니다. name 프로퍼티는 인스턴스의 슬롯(1) 에 존재하고 있습니다. name 프로퍼티를 출력하는 메소드인 printName()을 모든 인스턴스에 선언하게 되면 동일한 함수임에도 인스턴스마다 선언되게 됩니다. 그래서 우리는 모든 인스턴스가 참조할 수 있는 영역인, 생성자 함수 Human의 Prototype Object 내부에 printName()을 선언하였습니다(3). 이후 인스턴스 chanwoo가 printName()을 호출한 상황입니다. 인스턴스가 이 함수를 사용하기 위해 Prototype을 사용하는 과정은 파란색 숫자 순입니다.
1. 먼저 위 코드에서 A, chanwoo.printName()을 수행하는 과정을 설명하겠습니다.
- 인스턴스
chanwoo의 슬롯에printName()이 존재하는지 확인합니다. 이 슬롯은 인스턴스 각각의 영역으로,name프로퍼티만 존재합니다.
-
Prototype Link를 통해 자신의 원형 객체인
Human의 Prototype Object에 접근합니다.- 모든 객체가 Prototype Link,
[[Prototype]]프로퍼티를 가지기 때문에 생성자 함수Human으로 생성되는 모든 인스턴스가Human의 Prototype Object에 접근이 가능합니다.
- 모든 객체가 Prototype Link,
Human의 Prototype Object 내에printName()이 존재하는지 확인합니다.
printName()메소드가 존재하기 때문에 Prototype Link를 따라 더 조회하지 않고 해당 메소드를 수행합니다.
2. 다음은 B, chanwoo.hasOwnProperty('name')을 수행하는 과정을 설명하겠습니다.
- 1-3까지 위와 동일,
printName()대신,hasOwnProperty()로 바꿔서 이해하시면 되겠습니다.
-
Human의 Prototype Object 내에 존재하지 않으므로 Prototype Link에 따라 모든 Prototype Object의 원형인Object의 Prototype Object에 접근합니다.Object의 Prototype Object 는 모든 Prototype Object가 Prototype Link로 참조하고 있기 때문에 모든 객체가Object의Prototype Object가 가진 프로퍼티와 메소드를 사용할 수 있습니다.
hasOwnProperty()메소드가 존재하기 때문에 Prototype Link를 따라 더 조회하지 않고 해당 메소드를 수행합니다.
3. 마지막으로 C, chanwoo.printNickName()과 같이 Prototype Chain 상에 존재하지 않는 메소드를 호출하는 경우입니다.
- 1-4까지 위 과정과 동일합니다.
Object의 Prototype Object에 존재하지 않아 Prototype Link를 따라 Prototype Object에 접근합니다. 이때, Prototype Link가null값을 가리키고 있습니다. Prototype Chain의 끝에 도달하였기 때문에 에러를 반환합니다.
Prototype에서 주의할점
-
생성자 함수를 통해 인스턴스를 선언한 이후에 Prototype Object가 가진 프로퍼티와 메소드가 변경되더라도 Prototype Link를 통해 가리키고 있는 모든 인스턴스에게 변경이 반영됩니다.
생성자 함수를 통한 객체 생성과정을 설명하면서 인스턴스 생성 시점에 Prototype Object가 Prototype Link에 바인딩 된다고 설명하였습니다. 이때 중요한 것은 인스턴스 자신만의 Prototype Object가 생성되는 것이 아닌, Prototype Link를 통해 공통의 Prototype Object를 그저 참조만 하고 있다는 것입니다. Prototype Object 내의 변화가 존재하더라도 참조하고 있는 포인터 Prototype Link는 그대로이기 때문에 그 변화가 인스턴스의 생성 시점과는 무관하게 반영될 수 있는 것입니다.
function Human(name) { this.name = name; } Human.prototype.greet = function () { console.log(`hi, ${this.name}`); } const chanwoo = new Human('chanwoo'); const chanjong = new Human('chanjong'); chanwoo.greet(); // .... 1 // 인스턴스 생성 이후에 Prototype Object의 메소드를 변경하였다. Human.prototype.greet = function () { console.log(`안녕, ${this.name}`); } chanwoo.greet(); // .... 2 chanjong.greet(); // .... 3
-
동일한 이름의 프로퍼티와 메소드가 존재하더라도 Prototype Chain에서 우선적으로 조회되는 프로퍼티와 메소드를 사용합니다.
주의할 것은 참조하기 위한 Prototype Chain의 순서입니다. Prototype Chain은
Human인스턴스의 슬롯 →HumanPrototype Object →ObjectPrototype Object →null의 순으로 참조하면서 사용하고자 하는 프로퍼티, 메소드를 조회합니다. 동일한 프로퍼티, 메소드가 슬롯마다 정의될 수 있지만 Prototype Chain의 순서상 앞의 슬롯에 정의된 프로퍼티와 메소드가 우선으로 호출됩니다.function Human(name) { this.name = name; this.printName = function () { console.log(`${this.name} by instance`); // ------ 1 } } // 생성자 함수의 prototype 프로퍼티를 통해서 메소드를 추가하였다. Human.prototype.printName = function () { console.log(`${this.name} by proto`); // ------ 2 } const chanwoo = new Human('chanwoo'); chanwoo.printName(); // ----- A
예시는 위의 예시에서 동일한 이름을 가진
printName()메소드가 인스턴스에도 선언되어있는 것을 볼 수 있습니다. 즉, 위 그림에서 살색의 1번 영역과 3번 영역 모두 동일한 이름의printName()메소드가 선언된 상태입니다. 하지만 Prototype Chain의 순서상, 인스턴스의 슬롯인 1번을 먼저 조회하기 때문에 결과는 1번 줄이 수행됩니다. 이는 클래스의 메소드 오버라이딩의 개념과 동일하게 동작합니다.
-
객체 생성 방식에 따라 Prototype Link에 바인딩되는 Prototype Object가 다릅니다.
- 생성자 함수와
new연산자
생성되는 객체의 Prototype Link는 생성자 함수의 Prototype Object가 바인딩됩니다.
{}object initializer notation
생성되는 객체의 Prototype Link는 최상위 Prototype Object인ObjectPrototype Object가 바인딩 됩니다.
Object.create(<proto>)
생성되는 객체의 Prototype Link는 인자로 전달되는*proto*에 바인딩됩니다.
- 생성자 함수와
그래서 Prototype Chain을 말하자면..
Prototype Object와 Prototype Link를 통해 프로퍼티와 메소드의 상속 방식을 정의하였다면, Prototype Chain은 상속된 프로퍼티와 메소드를 사용하는 방식이라고 볼 수 있습니다. 프로퍼티나 메소드가 자신의 슬롯에 존재하지 않는 경우, Prototype Link를 통해 자신의 원형 객체 Prototype Object에 접근하여 조회하는 방식이 선형적으로 수행되기 때문에 Prototype Chain이라고 부르게 되었습니다.
끝으로
JS의 객체에 대해 깊이 있게 다뤄보았습니다. 객체의 생성과정, 원형 객체 Prototype Object, 원형 객체 포인터 Prototype Link, 그리고 이 둘을 통해 상속받는 메소드와 프로퍼티를 조회하는 방식 Prototype Chain에 관해 설명하였습니다. 다음 포스팅은 객체 생성과정에서 나왔던 this에 관해 설명해보도록 하겠습니다.
