본문으로 건너뛰기

prototype, __proto__, constructor, instanceof의 관계

무료2022-01-04#JS#__proto__和prototype#原型和原型链#prototype和constructor#prototype和instanceof#Javascript Object Layout

prototype, proto, constructor, instanceof의 관계 완벽하게 이해하기

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.prototypex.__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 비교는 그 역할이 매우 제한적입니다.

댓글

아직 댓글이 없습니다

댓글 작성