← 글 목록

notes

19장. 프로토타입

모던 JS Deep Dive — 프로토타입 체인·상속과 객체 동작 핵심 정리.

참고: 이웅모 《모던 자바스크립트 Deep Dive》 19장 · [MDN Inheritance and the prototype chain](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Inheritance_and_the_prototype_chain)

JS 객체는 복사 상속이 아니라 위임 체인이다. 인스턴스에 없으면 Constructor.prototype으로, 없으면 Object.prototype으로 올라간다. class만 외우면 “상속된다”에서 끝나는데, 디버깅할 때 드러나는 건 체인이다. 이 장이 그 체인의 전체 지도다.

flowchart BT
  I[인스턴스] -->|[[Prototype]]| P[Ctor.prototype]
  P -->|[[Prototype]]| O[Object.prototype]
  O --> N[null]

19.1 객체지향 프로그래밍 — 내가 붙잡은 말

명령형 목록이 아니라 상태 + 행동을 객체로 묶고, 비슷한 객체가 행동을 공유하게 만드는 쪽. JS는 클래스 기반이 아니라 프로토타입 기반이 본체다. class는 그 위에 올린 친절한 문법(25장).

19.2 상속과 프로토타입

같은 메서드를 인스턴스마다 복사하면 메모리·수정 비용이 아프다. 프로토타입에 한 번 두고 위임한다. “부모 객체의 능력을 빌린다”가 상속이고, 빌리는 길이 [[Prototype]] 링크다.

function Animal(name) {
  this.name = name;
}
Animal.prototype.hello = function () {
  return this.name;
};
const dog = new Animal('nana');
console.log(dog.hello()); // nana — 메서드는 프로토타입에
console.log(Object.getPrototypeOf(dog) === Animal.prototype); // true

19.3 프로토타입 객체

모든 객체는 [[Prototype]] 내부 슬롯을 갖는다. 그 슬롯이 가리키는 객체가 프로토타입. 함수(constructor)는 prototype 프로퍼티로 “인스턴스가 위임할 객체”를 들고 있다. 삼각형:

  • 인스턴스 → [[Prototype]] → Ctor.prototype
  • Ctor.prototype.constructor → Ctor
  • Ctor.prototype 자체도 위로 Object.prototype까지

19.3.1 __proto__ 접근자

레거시 창구. obj.__proto__로 [[Prototype]]을 읽·쓸 수 있는 환경이 많다. 진단용. 생성·연결은 Object.create / Object.getPrototypeOf / Object.setPrototypeOf(최후) / new·class.

19.3.2 함수의 prototype 프로퍼티

constructor만 가진다(17). new Foo() 시 인스턴스의 [[Prototype]]이 Foo.prototype을 가리킨다.

19.3.3 constructor 프로퍼티

Foo.prototype.constructor === Foo가 기본. 교체·직접 상속 때 깨질 수 있어, 필요하면 다시 맞춰 준다.

19.4 리터럴로 만든 객체의 생성자·프로토타입

{}로 만든 객체의 프로토타입은 Object.prototype. constructor는 Object. 배열 리터럴은 Array.prototype, 함수는 Function.prototype — 리터럴도 “어떤 생성자 계열인지”가 정해진다.

const o = {};
console.log(Object.getPrototypeOf(o) === Object.prototype); // true
console.log(o.constructor === Object); // true

19.5 프로토타입의 생성 시점

19.5.1 사용자 정의 생성자

함수 정의가 평가될 때 prototype 객체가 함께 준비된다. 그 안에 constructor가 연결된다.

19.5.2 빌트인 생성자

엔진 기동 시 Object/Array 등의 프로토타입이 이미 있다. 그래서 'hi'.toUpperCase()가 된다(21 래퍼 + String.prototype).

19.6 객체 생성 방식과 프로토타입 결정

생성 경로마다 위임 대상이 갈린다.

19.6.1 객체 리터럴 → Object.prototype

19.6.2 new Object() → 마찬가지로 Object.prototype 계열

19.6.3 new F() → F.prototype

function F() {}
const a = {};
const b = new Object();
const c = new F();
console.log(Object.getPrototypeOf(a) === Object.prototype);
console.log(Object.getPrototypeOf(c) === F.prototype);

WHY: “객체니까 다 같다”가 아니다. instanceof·메서드 공유가 이 결정에 달렸다.

19.7 프로토타입 체인

프로퍼티를 찾으면 자기 → 프로토타입 → … → Object.prototype → null. 체인을 따라가며 찾는다. 이게 상속의 실체.

console.log(dog.toString); // Object.prototype.toString을 위임
console.log(Object.getPrototypeOf(Object.prototype)); // null — 끝

19.8 오버라이딩과 프로퍼티 섀도잉

인스턴스에 같은 키를 쓰면 자기 것이 체인을 가린다(섀도잉). 부모 메서드는 그대로 있다.

dog.hello = function () { return 'woof ' + this.name; };
console.log(dog.hello()); // 인스턴스 쪽
delete dog.hello;
console.log(dog.hello()); // 다시 프로토타입

부모 것을 호출하려면 Animal.prototype.hello.call(this) 또는 class의 super.

19.9 프로토타입의 교체

19.9.1 생성자 쪽 교체

F.prototype = { … }로 갈아끼우면 그 이후 new F()만 새 프로토를 본다. 이미 만든 인스턴스는 옛 링크.

function Foo() {}
const old = new Foo();
Foo.prototype = { x: 1 };
const neu = new Foo();
console.log(Object.getPrototypeOf(old) === Object.getPrototypeOf(neu)); // false
// constructor 복구를 잊기 쉬움
Foo.prototype.constructor = Foo;

19.9.2 인스턴스 쪽 교체

Object.setPrototypeOf(obj, proto) 또는 리터럴 __proto__. 성능·가독성 이유로 남용 금지. 설계 단계에선 Object.create.

19.10 instanceof

obj instanceof Ctor ≈ 체인을 따라가며 Ctor.prototype이 나오는지. Ctor 자체를 보는 게 아니라 prototype 객체가 체인에 있는지가 기준.

console.log(dog instanceof Animal); // true
console.log(dog instanceof Object); // true

교체로 prototype을 바꾸면 instanceof 결과도 바뀐다.

19.11 직접 상속

19.11.1 Object.create

명시적으로 위임 대상을 고르는 공장. new 없이 체인만 잇는다.

const proto = { kind: 'base', hello() { return this.kind; } };
const child = Object.create(proto);
child.kind = 'child';
console.log(child.hello()); // child — this는 child, 메서드는 proto
const dict = Object.create(null); // 순수 사전 — toString 없음

19.11.2 리터럴 __proto__

{ __proto__: proto, … } 문법. 동작은 create와 비슷하나, 스타일·린트상 create 선호.

19.12 정적 프로퍼티/메서드

생성자 함수(또는 class) 자신에 붙는 것. 인스턴스 체인으로는 안 보인다.

function User(name) { this.name = name; }
User.createGuest = function () { return new User('guest'); };
const g = User.createGuest();
// g.createGuest — undefined

Object.keys / Array.isArray / Promise.resolve가 이 계열.

19.13 프로퍼티 존재 확인

19.13.1 in 연산자

체인까지 본다. toString in {} → true.

19.13.2 hasOwnProperty / Object.hasOwn

자기 것만. 프로토타입 키와 충돌하는 사전에는 Object.create(null) 또는 Object.hasOwn.

const person = { name: 'Lee' };
console.log('name' in person); // true
console.log('toString' in person); // true — 체인
console.log(Object.hasOwn(person, 'toString')); // false

19.14 프로퍼티 열거

19.14.1 for…in

자기 + 체인 enumerable. 순서는 정수 인덱스 등 규칙 있음. 프로토타입 오염에 민감.

19.14.2 Object.keys / values / entries

자기 자신의 enumerable 문자열 키만. 실무 기본.

for (const k in person) {
  if (!Object.hasOwn(person, k)) continue; // 옛 패턴
}
console.log(Object.keys(person)); // ['name']

생성자·prototype·인스턴스 삼각형 (복기)

function Animal(name) { this.name = name; }
Animal.prototype.hello = function () { return this.name; };
const dog = new Animal('nana');
// dog ──[[Prototype]]──▶ Animal.prototype ──▶ Object.prototype ──▶ null
// Animal.prototype.constructor === Animal

메서드는 프로토타입, 상태는 인스턴스. 17에서 공장을 만들고 여기서 체인을 탄다.

예제 — 왜 이렇게 쳐 봤냐

// 위임으로 타입 확장
function Dog(name) {
  Animal.call(this, name);
}
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
Dog.prototype.bark = function () { return 'wal'; };
const d = new Dog('coco');
console.log(d.hello(), d.bark(), d instanceof Dog, d instanceof Animal);

WHY: create로 체인만 잇고 constructor를 복구한다. class extends가 이 보일러플레이트를 줄인다(25).

// 사전 객체
const cache = Object.create(null);
cache['toString'] = 1; // Object.prototype과 충돌 없음
console.log(Object.keys(cache));

헷갈리던 장면 / 함정

  • class만 알고 prototype을 “옛것”으로 취급 → 스택·라이브러리·트랜스파일 결과에 그대로 남음
  • __proto__로 상속 “설정” 남용
  • prototype 교체 후 기존 인스턴스·instanceof 혼란
  • in vs hasOwn 혼동
  • for…in으로 객체 복사 → 프로토 키까지 복사
  • 인스턴스에 메서드 정의 → 공유 실패(17)

표

질문 → 자답

Q. 복사가 아니라 위임이면 부모가 바뀌면? A. 프로토타입 객체를 뮤테이션하면 위임 중인 인스턴스 조회 결과가 바뀐다. 공유의 양날.

Q. null 프로토타입은 언제? A. 키 저장소·맵 대용. toString 등 오염 방지.

Q. setPrototypeOf 성능? A. 엔진 최적화에 불리할 수 있다. 생성 시 create/new/class로 고정.

웹 보강 메모

MDN prototype chain 다이어그램과 Object.hasOwn(ES2022)를 확인했다. 코드베이스에서 for…in·__proto__·instanceof 오용을 검색해 keys/hasOwn으로 옮길 후보를 표시했다.

실무로 바로

  • 도메인 타입: class 또는 생성자+prototype
  • 사전: Object.create(null) 또는 Map
  • 열거: Object.keys / Object.hasOwn
  • 상속: extends 우선, 레거시는 create+constructor 복구
  • 디버그: Object.getPrototypeOf 반복

여기서 잠깐 💁‍♂️

25장 class는 이 체인을 설탕으로 감싼다. 체인을 모르면 extends·super가 마법으로 남는다.