JavaScript의 프로토타입 체인에 대해 이야기할 때, 전설적인 그림이 하나 있습니다.

(위 그림은 JavaScript Object Layout에서 가져왔습니다.)
그림 속의 관계 살펴보기
함수 두 그룹, 객체 세 그룹, 그리고 파생된 두 그룹까지 총 7그룹의 관계가 그림에 나타나 있습니다.
먼저 함수를 보겠습니다:
/**
* 함수의 두 가지 관계
*/
function Cat() {}
// 0번째 관계
// 인스턴스의 constructor는 생성자 함수입니다.
new Cat().constructor === Cat
// 생성자 함수의 constructor는 생성자 함수를 만든 존재입니다.
Cat.constructor === Function
// 첫 번째 관계: 인스턴스의 __proto__는 생성자 함수의 prototype을 가리킵니다.
// 이 관계는 프로토타입 체인 상속의 핵심 동작을 생각하면 기억하기 쉽습니다.
new Cat().__proto__ === Cat.prototype
// 두 번째 관계: 생성자 함수의 prototype의 constructor는 다시 자기 자신입니다.
Cat.prototype.constructor === Cat
다음으로 객체를 보겠습니다:
/**
* 객체의 세 가지 관계
*/
// 첫 번째 관계는 여전히 성립합니다.
new Object().__proto__ === Object.prototype
// 두 번째 관계도 여전히 성립합니다.
Object.prototype.constructor === Object
// 세 번째 관계: Object의 prototype의 __proto__는 null입니다.
Object.prototype.__proto__ === null
마지막으로 파생된 관계들입니다:
/**
* 파생된 두 가지 관계
*/
// 네 번째 관계: 생성자 함수는 new Function을 통해 만들어집니다.
// 따라서 생성자 함수의 __proto__는 Function의 prototype을 가리킵니다.
Cat.__proto__ === Function.prototype
// 위와 동일합니다.
Object.__proto__ === Function.prototype
// 다섯 번째 관계: 원래 위와 동일해야 하지만, 매우 특수하므로 따로 뺍니다.
// Function 또한 new Function을 통해 만들어집니다!!
// Function.constructor === Function 즉, Function의 생성자 함수는 자기 자신입니다. 따라서
// Function의 __proto__는 Function의 prototype을 가리킵니다.
Function.__proto__ === Function.prototype
// 여섯 번째 관계: Function의 prototype의 __proto__는 Object의 prototype입니다.
Function.prototype.__proto__ === Object.prototype
prototype, __proto__, constructor의 삼각 관계를 어떻게 이해해야 할까요?
이렇게 이해해 보세요:
- 생성자 함수는 인스턴스를 만드는 기계입니다.
- 인스턴스가 생성 과정에서 어떤 속성을 얻을지는 기계가 어떤 속성을 가지고 있느냐에 달려 있습니다.
- 이 속성 뭉치는 생성자 함수의
prototype속성에 들어 있습니다 (이를 프로토타입 객체라고 부릅니다). new로 인스턴스를 만들 때, 인스턴스에__proto__를 연결하여 인스턴스가 이 속성 뭉치를 찾아갈 수 있게 합니다.
특수한 경우:
- 생성자 함수와 그
prototype은 양방향 관계이며, 반대로 가리키는 속성을constructor라고 부릅니다.
prototype과 __proto__를 어떻게 구분하나요?
본질적으로 가장 큰 차이점은 '누가 사용하는가'에 있습니다.
prototype은 인스턴스가 사용할 프로토타입 객체이며, 생성자 함수만 가지고 있습니다.__proto__는 자신의 프로토타입 객체를 가리키는 속성으로, 모든 객체가 가지고 있습니다.x.prototype과x.__proto__의 차이는 전자는 인스턴스에게 주기 위한 것이고 (new x를 하지 않으면 쓸모가 없음), 후자는 자신이 사용하기 위한 것이라는 점입니다.
둘 다 프로토타입과 관련이 있는데 이름은 어떻게 구분할까요?
prototype은 (자식) 인스턴스가 사용할 속성 뭉치를 담고 있으므로 프로토타입 객체라고 부릅니다.__proto__는 프로토타입 체인을 연결하는 역할을 하므로 흔히 프로토타입이라고 부릅니다.
instanceof는 인스턴스와 클래스(생성자 함수)의 관계를 어떻게 판별하나요?
MDN에 instanceof를 완벽하게 설명하는 문장이 있습니다:
instanceof 연산자는 생성자의 prototype 속성이 객체의 프로토타입 체인 어딘가에 나타나는지 테스트합니다.
(instanceof에서 발췌)
이 문장 그대로입니다. 이 원리에 따라 직접 instanceof를 구현해 볼 수 있습니다:
function insOf(obj, Ctor) {
let proto = obj;
// while (proto = obj.__proro__) {
while (proto = Object.getPrototypeOf(proto)) {
if (Ctor.prototype === proto) {
return true;
}
}
return false;
}
// Case 1
class A {}
insOf(new A(), A)
// Case 2
function Cat() {}
insOf(new Cat(), Cat)
insOf(new Cat(), Object)
constructor와 상속에 대해 더 깊이 알아보기
0번째 관계에서 인스턴스의 constructor는 생성자 함수라는 것을 배웠습니다:
// 인스턴스의 constructor는 생성자 함수입니다.
new Cat().constructor === Cat
그렇다면 인스턴스의 constructor 속성은 어디서 온 것일까요?
맞습니다. new Cat()으로 인스턴스를 만들 때, 생성자 함수의 프로토타입 객체에서 가져온 것입니다:
new Cat().constructor === Cat.prototype.constructor
따라서 전형적인 ES5 상속 방식에서는 다음과 같이 처리합니다:
function extend(Sub, Super) {
// 1. Super.prototype을 프로토타입으로 가지는 익명 객체를 생성하여 반환합니다.
var proto = Object.create(Super.prototype);
// 2. constructor 속성을 올바르게 수정합니다.
proto.constructor = Sub;
// 3. 자식 클래스의 인스턴스가 익명 객체의 프로토타입 속성에 접근할 수 있도록 합니다.
Sub.prototype = proto;
}
constructor가 가리키는 대상을 수정하는 것은 instanceof 연산자의 결과에 영향을 주지 않습니다. 수정을 하는 이유는 단지 constructor 속성값을 올바르게 유지하기 위해서입니다.
// 만약 constructor를 수정하지 않으면, 자식 클래스 인스턴스의 constructor는 B가 아닌 A가 됩니다.
function Sub() {}
function Super() {}
var proto = Object.create(Super.prototype);
Sub.prototype = proto;
// 이렇게 되면 매우 이상해 보입니다 (인스턴스의 constructor가 생성자 함수와 일치하지 않음).
new Sub().constructor !== Sub;
// 그래서 올바르게 수정해 줍니다.
proto.constructor = Sub;
// 이제 정상입니다.
new Sub().constructor === Sub;
몇 가지 상식
1. 화살표 함수는 프로토타입 객체가 없습니다 (따라서 화살표 함수는 생성자 함수로 사용할 수 없습니다).
(() => 1).prototype === undefined
2. 네이티브 객체의 프로토타입은 너무 깊게 파고들지 마세요. 확실하지 않을 수 있습니다.
Math.__proto__ === Object.prototype
Math.max.__proto__ === Function.prototype
Window.__proto__ === EventTarget
console.__proto__ === ?
3. 함수의 prototype을 비교하는 것은 어떤 의미가 있을까요?
npm 다운로드 수가 매우 높은 is 모듈에는 다음과 같은 오래된 코드가 있습니다:
if (type === '[object Function]') {
return value.prototype === other.prototype;
}
보시다시피 두 함수의 prototype을 비교하여 함수의 동등성을 판단하고 있습니다. 하지만 조금만 생각해 봐도 이는 신뢰하기 어렵다는 것을 알 수 있습니다. 예를 들어:
(x => x).prototype === (x => x + 1).prototype === undefined
그래서 제가 라이브러리 작성자에게 질문했습니다. 몇 번의 질답 끝에, prototype 비교는 생성자 함수나 클래스가 동일한지 확인하는 데 사용될 수 있음을 확인했습니다 (단, prototype은 수정 가능하다는 점에 유의해야 합니다):
class Dog {}
class Cat {}
function Button() {}
function Panel() {}
Dog.prototype !== Cat.prototype
Button.prototype !== Panel.prototype
함수가 생성자 함수나 클래스가 아니라면 prototype을 비교하는 것은 아무런 의미가 없습니다. OOP가 아주 보편적이지 않은 JS 환경에서 라이브러리 내의 함수 prototype 비교는 그 역할이 매우 제한적입니다.
아직 댓글이 없습니다