JavaScript · 8장 프로토타입
프로토타입 체인: 없는 속성은 위에서 빌려 와요
빈 배열 []에도 map이 있고, 빈 객체 {}에도 toString이 있어요. 우리가 넣은 적 없는 이 메서드들은 어디서 왔을까요? 답은 객체마다 숨어 있는 연결, 프로토타입이에요. 클래스 문법(9장)도 결국 이 연결 위에서 동작해서, 한 번 그림으로 잡아 두면 오류 메시지와 라이브러리 코드가 훨씬 잘 읽혀요.
01. 없으면 연결된 객체에서 찾아요
모든 객체는 숨은 연결 하나를 가지고 있어요. 이 연결이 가리키는 객체를 그 객체의 프로토타입이라고 해요. 속성을 읽을 때 자기에게 없으면 프로토타입에서 찾고, 거기도 없으면 그 프로토타입의 프로토타입에서 찾아요. 이렇게 이어진 줄이 프로토타입 체인이고, 끝(null)까지 없으면 undefined예요.
Object.create(proto)는 proto를 프로토타입으로 가진 새 객체를 만들어요. 연결된 객체는 Object.getPrototypeOf(obj)로 확인해요.
const base = {
kind: '회원',
greet() { return `안녕, ${this.name}`; },
};
const sky = Object.create(base); // sky의 프로토타입은 base
sky.name = '하늘';
console.log(sky.greet()); // 안녕, 하늘 — greet는 base에서 빌려 와요
console.log(sky.kind, sky.fly); // 회원 undefined
console.log(Object.getPrototypeOf(sky) === base); // true
console.log(Object.keys(sky), Object.hasOwn(sky, 'kind')); // ['name'] false — 자기 것만
sky.kind = '관리자'; // 쓰기는 늘 자기 자신에게
console.log(sky.kind, base.kind); // 관리자 회원 — base는 그대로
두 가지를 기억해요. 빌려 온 메서드 안의 this는 메서드를 가진 객체(base)가 아니라 부른 객체(sky)예요(this). 그리고 읽기는 체인을 따라 올라가지만 쓰기와 지우기는 늘 자기 자신에게만 해요. 프로토타입에 같은 이름이 있어도 자기 칸을 새로 만들어 가릴 뿐, 위쪽 객체는 바뀌지 않아요.
02. 매일 쓰는 배열과 객체도 체인이에요
배열 리터럴 [1, 2]를 만들면 그 배열의 프로토타입은 Array.prototype이에요. map · filter · push는 배열마다 있는 게 아니라 Array.prototype에 하나씩만 있고, 모든 배열이 빌려 써요. 그 위에는 모든 객체가 공유하는 Object.prototype(toString, hasOwnProperty…)이 있고, 그 위는 null이에요.
const arr = [1, 2];
console.log(Object.getPrototypeOf(arr) === Array.prototype); // true
console.log(Object.getPrototypeOf(Array.prototype) === Object.prototype); // true
console.log(Object.getPrototypeOf(Object.prototype)); // null — 끝
console.log(Object.hasOwn(arr, 'map'), typeof arr.map); // false function — 빌려 써요
console.log(arr.toString()); // 1,2 — Array.prototype의 것
console.log(arr.fly); // undefined — 끝까지 없어요
문자열 · 숫자 같은 원시값도 메서드를 부를 때는 잠깐 String.prototype · Number.prototype을 빌려요. 'abc'.toUpperCase()가 되는 이유예요. "x.foo is not a function" 오류는 결국 "체인 끝까지 찾았는데 foo가 함수가 아니었다(없었다)"는 뜻이에요.
03. 생성자와 class가 체인을 만드는 법
함수에는 prototype이라는 이름의 속성이 있어요. new F()로 만든 객체는 프로토타입이 F.prototype으로 연결돼요. 그래서 F.prototype에 메서드를 두면 new로 만든 모든 객체가 하나를 함께 써요. 4장에서 클래스의 메서드를 모든 객체가 함께 쓴다고 했던 이유가 이거예요(클래스로 이어지기). class의 메서드는 자동으로 클래스.prototype에 들어가요.
function OldUser(name) { this.name = name; }
OldUser.prototype.hi = function () { return `hi, ${this.name}`; }; // 예전 방식
class User {
constructor(name) { this.name = name; }
greet() { return `안녕, ${this.name}`; } // User.prototype에 들어가요
}
const a = new OldUser('하늘');
const b = new User('가람');
console.log(a.hi(), Object.getPrototypeOf(a) === OldUser.prototype); // hi, 하늘 true
console.log(Object.getPrototypeOf(b) === User.prototype); // true
console.log(Object.hasOwn(b, 'greet'), Object.hasOwn(User.prototype, 'greet')); // false true
이름이 헷갈리기 쉬워요. F.prototype은 "F로 만들 객체들의 프로토타입이 될 객체"이지, F 자신의 프로토타입이 아니에요. 오래된 코드에서 보이는 obj.__proto__는 프로토타입을 읽고 쓰던 옛 방법이고, 지금은 Object.getPrototypeOf · Object.create를 써요. 상속 관계를 직접 엮을 일은 이제 거의 없고 class ... extends(9장)로 해요.
기본 설정을 프로토타입으로 두고, 사용자가 바꾼 값만 자기 칸에 담는 설정 객체예요. "사용자가 바꾼 설정"만 보여 줘야 하는데 기본값까지 섞여 나와요. 고쳐서 ['theme']가 나오게 해 보세요. 기본값은 여전히 읽을 수 있어야 해요(ko).
const defaults = { theme: 'light', lang: 'ko' };
const settings = Object.create(defaults);
settings.theme = 'dark'; // 사용자가 바꾼 값
const changed = [];
for (const key in settings) changed.push(key);
console.log(changed);
console.log(settings.lang);
해답 보기
for...in은 자기 속성뿐 아니라 체인에서 물려받은 속성(열거 가능한 것)까지 돌아요. 자기 것만 필요하면 Object.keys를 쓰거나, for...in 안에서 Object.hasOwn으로 걸러요.
const changed = Object.keys(settings);
// ['theme']
// ko
04. 실전 포인트
- 언제 쓰나요? 직접 체인을 만들 일보다 읽을 일이 많아요. "
is not a function" 오류, 라이브러리의prototype코드,for...in에 낯선 키가 섞일 때 체인을 떠올려요. 객체가 무엇을 물려받는지는Object.getPrototypeOf로 확인해요. - 대신 쓰는 것: 같은 메서드를 가진 객체들은
class(9장), 상속은extends를 써요. 객체를 돌 때는Object.keys·entries(객체를 배열로), 자기 속성 확인은Object.hasOwn을 써요. - 흔한 실수:
for...in으로 돌다가 물려받은 키를 처리하는 것,F.prototype을 F 자신의 프로토타입으로 착각하는 것,__proto__로 프로토타입을 바꾸는 것(느리고 위험해요), 프로토타입의 배열 · 객체 속성을 고쳐서 모든 객체가 함께 바뀌는 것이에요.
정리
- 객체마다 프로토타입이라는 숨은 연결이 있고, 없는 속성은 체인을 따라 위에서 찾아요. 끝은
null이에요. - 읽기는 체인을 따라가지만, 쓰기 · 지우기는 늘 자기 자신에게 해요.
- 배열은
Array.prototype→Object.prototype→null로 이어져서 메서드를 빌려 써요. new F()와class로 만든 객체의 프로토타입은F.prototype이에요. 메서드는 그곳에 하나만 있어요.
Object.hasOwn([], 'map')은 true일까요?
false예요. map은 배열마다 있는 게 아니라 Array.prototype에 하나 있고, 배열은 체인을 따라 빌려 써요.
프로토타입에 있는 속성을 자식 객체에서 child.kind = 'x'로 바꾸면, 프로토타입의 값도 바뀔까요?
바뀌지 않아요. 쓰기는 늘 자기 자신에게 해서, 자식에게 kind 칸이 새로 생기고 프로토타입의 값을 가릴 뿐이에요.
class User의 greet 메서드는 어디에 있을까요?
User.prototype에 하나 있어요. new User()로 만든 객체들은 프로토타입이 User.prototype이라서 체인을 따라 같은 greet를 빌려 써요.