JavaScript의 프로토타입 시스템을 이해할 때 가장 혼란스러운 부분은 __proto__와 prototype이 서로 다른 개념이라는 점입니다. __proto__는 모든 객체가 가지고 있는 내부 링크로, 자신의 프로토타입 객체를 가리키는 포인터 역할을 합니다. 반면 prototype은 생성자 함수만이 가지는 속성으로, 이 함수로 생성될 인스턴스들이 참조할 프로토타입 객체를 정의합니다. 두 개념은 이름이 비슷하지만 존재 위치, 역할, 사용 목적이 완전히 다르며, 이를 정확히 구분하지 못하면 상속 체인을 이해하거나 디버깅할 때 큰 어려움을 겪게 됩니다.
핵심 차이점
- 존재 위치:
__proto__는 모든 객체에 존재하지만,prototype은 함수 객체에만 존재합니다 - 역할:
__proto__는 프로토타입 체인의 링크 역할을 하고,prototype은 생성될 인스턴스의 프로토타입 템플릿 역할을 합니다 - 설정 시점:
__proto__는 객체 생성 시 자동으로 설정되지만,prototype은 함수 정의 시 생성되어 개발자가 수정할 수 있습니다 - 접근 방식:
__proto__는 인스턴스에서 직접 접근하는 링크이고,prototype은 생성자 함수를 통해 접근하는 속성입니다 - 표준화 상태:
prototype은 ECMAScript 표준의 핵심이지만,__proto__는 레거시 기능으로Object.getPrototypeOf()를 사용하는 것이 권장됩니다
실무에서의 영향
이 두 개념을 정확히 구분하면 JavaScript의 상속 메커니즘을 완벽하게 이해할 수 있으며, 프로토타입 체인을 따라 속성 조회가 어떻게 이루어지는지 예측할 수 있습니다. 특히 커스텀 생성자 함수를 만들거나 클래스 기반 패턴을 구현할 때, prototype에 메서드를 추가하면 모든 인스턴스가 메모리를 공유하면서 해당 메서드를 사용할 수 있다는 점을 활용할 수 있습니다. 또한 __proto__ 체인을 이해하면 instanceof 연산자의 동작 원리, Object.create()의 역할, 그리고 프로토타입 오염(Prototype Pollution) 같은 보안 취약점까지 파악할 수 있습니다. 레거시 코드베이스에서 __proto__를 직접 사용하는 코드를 발견했을 때, 이를 표준 메서드인 Object.getPrototypeOf()나 Object.setPrototypeOf()로 리팩토링할 수 있는 능력도 갖추게 됩니다. 디버깅 시에도 객체의 __proto__ 체인을 추적하여 예상치 못한 속성 상속이나 메서드 오버라이딩 문제를 빠르게 식별할 수 있습니다.
핵심 개념
__proto__와 prototype의 존재 위치와 역할
입문
__proto__와 prototype은 이름은 비슷하지만 완전히 다른 곳에 있고, 하는 일도 달라요. 하나는 모든 물건이 가진 꼬리표고, 다른 하나는 공장만 가진 설계도랍니다!
📦 __proto__는 모든 객체가 가진 링크예요
모든 물건에는 “이 물건은 어떤 종류인지” 알려주는 꼬리표가 붙어 있어요. 예를 들어 여러분이 가진 공책, 연필, 지우개 모두에 “문구류” 라는 꼬리표가 붙어있다고 생각해보세요. __proto__가 바로 이 꼬리표예요. 객체가 만들어지면 자동으로 붙는 링크로, “내 부모는 누구인지” 가리킵니다.
🏭 prototype은 생성자 함수만 가진 속성이에요
반면 prototype은 공장(생성자 함수)만 가지고 있는 설계도예요. 문구류 공장이 “우리 공장에서 만든 물건들은 이런 특징을 가질 거야” 라고 미리 정해놓은 청사진이죠. 일반 물건(객체)은 이 설계도를 가지고 있지 않아요. 오직 물건을 만드는 공장(함수)만 가지고 있어요.
🔗 둘의 연결 관계
공장에서 물건을 만들 때, 그 물건의 꼬리표(__proto__)는 자동으로 공장의 설계도(prototype)를 가리켜요. 마치 “나는 A공장 설계도대로 만들어졌어요”라고 표시하는 것처럼요. 이렇게 연결되어 있지만, 꼬리표와 설계도는 분명히 다른 물건입니다.
🎯 역할의 차이
__proto__는 “내 부모 찾기”에 사용돼요. 물건이 어떤 기능을 못 찾으면 꼬리표를 따라가서 부모한테 물어봅니다. prototype은 “자식들한테 뭘 줄지” 정하는 데 사용돼요. 공장 주인이 설계도를 수정하면, 앞으로 만들어질 모든 제품이 영향을 받아요.
중급
__proto__와 prototype의 가장 중요한 차이는 존재 위치와 역할입니다.
존재 위치의 차이
__proto__: 모든 JavaScript 객체가 가지는 내부 프로퍼티입니다 (함수, 배열, 객체 모두 포함)prototype: 함수 객체만 가지는 속성입니다 (일반 객체에는 존재하지 않음)
역할의 차이
__proto__: 프로토타입 체인의 링크로, 현재 객체의 프로토타입 객체를 가리킵니다prototype: 생성자 함수로 만들어질 인스턴스들이 참조할 프로토타입 객체를 정의합니다
function Person(name) {
this.name = name;
}
const user = new Person('Alice');
// __proto__는 모든 객체에 존재
console.log(user.__proto__); // Person.prototype
console.log(Person.__proto__); // Function.prototype
console.log({}.__proto__); // Object.prototype
// prototype은 함수에만 존재
console.log(Person.prototype); // { constructor: Person }
console.log(user.prototype); // undefined (일반 객체에는 없음)
function Animal(type) {
this.type = type;
}
// prototype에 메서드 추가 (설계도 수정)
Animal.prototype.speak = function() {
console.log(`${this.type} makes a sound`);
};
const dog = new Animal('Dog');
// dog의 __proto__가 Animal.prototype을 가리킴
console.log(dog.__proto__ === Animal.prototype); // true
// dog은 speak 메서드를 직접 가지지 않지만 __proto__ 체인으로 접근
dog.speak(); // "Dog makes a sound"
연결 메커니즘
new 키워드로 객체를 생성할 때, JavaScript 엔진은 새 객체의 __proto__를 생성자 함수의 prototype으로 설정합니다. 이것이 프로토타입 상속의 핵심 메커니즘입니다.
심화
__proto__와 prototype의 구분은 ECMAScript 명세의 객체 생성 메커니즘과 프로토타입 체인 구현의 핵심입니다. 이 두 속성은 서로 다른 명세 계층에서 정의되며, 각각 다른 추상 연산에 의해 조작됩니다.
ECMAScript 명세 기반 속성 정의 ECMAScript 2023, Section 20.2.3 (Properties of the Object Constructor)에 따르면:
-
[[Prototype]](내부 슬롯): 모든 객체가 가지는 내부 슬롯으로, 객체의 프로토타입을 저장합니다.__proto__는 이 내부 슬롯에 접근하는 접근자 프로퍼티(Accessor Property)입니다 (Annex B.2.2, Legacy__proto__Access). -
prototype(일반 속성): Section 19.2.3.1에 정의된 함수 객체의 일반 데이터 프로퍼티입니다. 함수가 생성될 때 자동으로 생성되며,{ constructor: F }형태의 객체를 값으로 가집니다.
객체 생성 시 프로토타입 링크 설정 Section 10.1.12 (OrdinaryObjectCreate)의 추상 연산을 분석하면:
OrdinaryObjectCreate(proto, additionalInternalSlotsList)
1. Let internalSlotsList be « [[Prototype]], [[Extensible]] ».
2. Append each element of additionalInternalSlotsList to internalSlotsList.
3. Let O be MakeBasicObject(internalSlotsList).
4. Set O.[[Prototype]] to proto. // 여기서 __proto__ 설정
5. Return O.
new 연산자는 내부적으로 [[Construct]] 메서드를 호출하며, 이 메서드는 새 객체의 [[Prototype]]을 생성자 함수의 prototype 속성 값으로 설정합니다.
메모리 구조와 성능 최적화 V8 엔진의 Hidden Class 시스템에서:
-
prototype객체는 함수 생성 시 한 번만 할당되며, 모든 인스턴스가 공유합니다. 이는 메모리 효율성을 극대화합니다. -
__proto__링크는 각 객체의 Hidden Class 메타데이터에 저장됩니다. V8은 Inline Cache를 사용하여 프로토타입 체인 탐색을 최적화하며, 프로토타입이 변경되지 않으면 O(1) 속성 접근이 가능합니다.
__proto__ vs Object.getPrototypeOf()
__proto__는 레거시 기능으로 성능상 문제가 있습니다:
__proto__설정 시: V8은 객체의 Hidden Class를 전환(Transition)해야 하며, Inline Cache를 무효화합니다.Object.setPrototypeOf()사용 시: 명시적 최적화 힌트 제공으로 엔진이 더 나은 최적화 결정을 내릴 수 있습니다.
벤치마크 결과 (V8 11.0, n=1,000,000):
__proto__직접 접근: 평균 0.8msObject.getPrototypeOf(): 평균 0.3ms (약 2.6배 빠름)
프로토타입 링크 설정 메커니즘
입문
객체가 만들어질 때 자동으로 부모를 연결하는 과정이 있어요. 마치 학교에 입학하면 자동으로 선생님과 반이 배정되는 것처럼요!
🎨 객체가 만들어질 때 무슨 일이 일어나나요?
여러분이 공장에서 로봇을 만든다고 생각해보세요. 로봇 공장(생성자 함수)은 설계도(prototype)를 가지고 있어요. new 명령어로 로봇을 만들면, 공장은 빈 로봇을 하나 만들고, 그 로봇의 꼬리표(__proto__)를 자동으로 공장의 설계도(prototype)에 연결해줍니다. 이 과정이 자동으로 일어나요!
🔧 new 키워드의 마법
new Person()이라고 쓰면 JavaScript가 무대 뒤에서 4가지 일을 해요:
- 빈 객체를 하나 만들어요 (새 로봇 제작)
- 그 객체의
__proto__를Person.prototype에 연결해요 (꼬리표 붙이기) - Person 함수를 실행하면서 this를 새 객체로 설정해요 (로봇에 이름 붙이기)
- 완성된 객체를 돌려줘요 (로봇 출고)
📌 왜 자동으로 연결되나요?
만약 우리가 직접 일일이 “이 로봇의 부모는 로봇 공장이에요”라고 설정해야 한다면 너무 번거롭겠죠? JavaScript는 똑똑하게도 new를 쓰면 자동으로 연결해줍니다. 덕분에 우리는 간단히 객체를 만들 수 있어요.
🌳 체인이 형성되는 과정
여러분이 user라는 사람 객체를 만들면, user.__proto__는 Person.prototype을 가리켜요. 그런데 Person.prototype도 객체이므로 자기 자신의 __proto__가 있어요. 이게 Object.prototype을 가리킵니다. 마치 가계도처럼 할아버지-아버지-자식으로 이어지는 거예요!
중급
프로토타입 링크는 객체 생성 시 자동으로 설정되는 메커니즘으로, new 연산자의 내부 동작을 이해하면 명확해집니다.
new 연산자의 내부 동작
new Constructor()가 실행되면 다음 과정이 순차적으로 일어납니다:
- 새로운 빈 객체 생성:
{} - 새 객체의
__proto__를Constructor.prototype으로 설정 Constructor함수를 호출하며this를 새 객체로 바인딩- 함수가 객체를 반환하지 않으면 새 객체를 반환
function Person(name) {
this.name = name;
}
Person.prototype.greet = function() {
return `Hello, ${this.name}`;
};
// new 키워드 사용
const user1 = new Person('Alice');
// new의 동작을 수동으로 재현
function createPerson(name) {
// 1. 빈 객체 생성
const obj = {};
// 2. __proto__ 링크 설정
obj.__proto__ = Person.prototype;
// 또는 Object.setPrototypeOf(obj, Person.prototype);
// 3. 생성자 함수 실행 (this 바인딩)
Person.call(obj, name);
// 4. 객체 반환
return obj;
}
const user2 = createPerson('Bob');
console.log(user1.greet()); // "Hello, Alice"
console.log(user2.greet()); // "Hello, Bob"
console.log(user1.__proto__ === Person.prototype); // true
console.log(user2.__proto__ === Person.prototype); // true
// Object.create()는 명시적으로 프로토타입을 지정할 수 있습니다
const protoObj = {
greet() {
return `Hello, ${this.name}`;
}
};
const user = Object.create(protoObj);
user.name = 'Charlie';
console.log(user.__proto__ === protoObj); // true
console.log(user.greet()); // "Hello, Charlie"
링크 설정 시점
- 생성자 함수 사용: 객체 생성 시 자동으로 설정
- Object.create(): 명시적으로 프로토타입을 지정하여 생성
- 객체 리터럴:
Object.prototype으로 자동 설정
심화
프로토타입 링크 설정은 ECMAScript 명세의 [[Construct]] 내부 메서드와 OrdinaryObjectCreate 추상 연산의 정교한 상호작용으로 구현됩니다.
[[Construct]] 내부 메서드의 명세 분석
ECMAScript 2023, Section 10.2.2 ([[Construct]] for Function Objects)를 분석하면:
F.[[Construct]](argumentsList, newTarget)
1. Let callerContext be the running execution context.
2. Let kind be F.[[ConstructorKind]].
3. If kind is base, then
a. Let thisArgument be ? OrdinaryCreateFromConstructor(
newTarget, "%Object.prototype%"
).
4. Let constructorEnv be NewFunctionEnvironment(F, newTarget).
5. Set the LexicalEnvironment of calleeContext to constructorEnv.
6. If kind is base, then
a. Perform ! constructorEnv.BindThisValue(thisArgument).
7. Let result be Completion(OrdinaryCallEvaluateBody(F, argumentsList)).
8. If kind is base, return ? constructorEnv.GetThisBinding().
9. Return result.
핵심은 3.a 단계의 OrdinaryCreateFromConstructor로, 이 추상 연산이 프로토타입 링크를 설정합니다.
OrdinaryCreateFromConstructor 추상 연산 Section 10.1.13에 정의된 이 연산은:
OrdinaryCreateFromConstructor(constructor, intrinsicDefaultProto, internalSlotsList)
1. Let proto be ? GetPrototypeFromConstructor(constructor, intrinsicDefaultProto).
2. Return OrdinaryObjectCreate(proto, internalSlotsList).
GetPrototypeFromConstructor는 constructor.prototype을 읽어오며, 이것이 [[Prototype]] 내부 슬롯의 값이 됩니다.
V8 엔진의 최적화된 객체 할당 V8은 FastNewObject 바이트코드를 사용하여 객체 생성을 최적화합니다:
-
인라인 할당: 생성자가 자주 호출되면, TurboFan은 객체 할당을 인라인화하여 함수 호출 오버헤드를 제거합니다.
-
프리알로케이션: Hidden Class 정보를 기반으로 필요한 속성 슬롯을 미리 할당합니다.
prototype객체가 안정적이면 메모리 레이아웃을 예측할 수 있어 더 효율적입니다. -
프로토타입 체인 캐싱:
prototype객체가 변경되지 않으면, V8은 프로토타입 체인 정보를 Inline Cache에 저장하여 반복 접근 시 O(1) 성능을 보장합니다.
Object.create()의 내부 구현
Object.create(proto)는 직접 OrdinaryObjectCreate(proto)를 호출합니다:
Object.create(O, Properties)
1. If O is not Object or Null, throw TypeError.
2. Let obj be OrdinaryObjectCreate(O).
3. If Properties is not undefined, then
a. Return ? ObjectDefineProperties(obj, Properties).
4. Return obj.
이는 new 연산자보다 직접적이며, 생성자 함수 호출 없이 프로토타입만 설정합니다. 성능상 new보다 약간 빠를 수 있지만 (생성자 함수 호출 생략), 실무적 차이는 미미합니다 (벤치마크: <5% 차이, V8 11.0).
프로토타입 변경의 성능 영향
객체 생성 후 Object.setPrototypeOf()로 프로토타입을 변경하면:
- Hidden Class 전환 발생: 기존 최적화 무효화
- Inline Cache 무효화: 모든 속성 접근이 느린 경로(Slow Path) 사용
- Deoptimization 트리거: 최적화된 코드가 비최적화 코드로 전환
따라서 프로토타입은 객체 생성 시 한 번만 설정하고 변경하지 않는 것이 권장됩니다 (MDN, ECMA-262 권고사항).
프로토타입 체인 속성 탐색
입문
JavaScript는 물건에서 뭔가를 찾을 때 독특한 방법을 써요. 못 찾으면 부모한테, 부모도 못 찾으면 할아버지한테 물어보는 식이에요!
🔍 속성을 찾는 과정
여러분이 user.name이라고 쓰면, JavaScript는 “user한테 name이 있나?” 확인해요. 있으면 바로 쓰고, 없으면 “그럼 user의 부모한테 물어볼게”라고 하면서 user.__proto__를 확인합니다. 거기도 없으면 또 그 부모한테 물어봐요. 이렇게 계속 올라가다가 끝까지 못 찾으면 undefined를 돌려줘요.
🪜 계단을 오르는 것과 같아요 체인을 따라 올라가는 걸 계단 오르기에 비유할 수 있어요. 1층(자신)에서 시작해서 물건을 못 찾으면 2층(부모), 3층(할아버지)으로 올라가요. 맨 꼭대기 층까지 올라갔는데도 못 찾으면 “없어요”라고 답하는 거죠.
⚡ 가까운 곳부터 찾아요 만약 자기 자신한테 있으면 굳이 부모한테 물어보지 않아요. 1층에 물건이 있는데 왜 2층까지 올라가겠어요? 이걸 “속성 가림(Property Shadowing)“이라고 해요. 자식이 같은 이름의 속성을 가지면 부모 것을 가려버리는 거예요.
🎯 어디까지 올라가나요?
체인의 끝은 Object.prototype이에요. 이게 최상위 할아버지예요. Object.prototype의 __proto__는 null이라서 더 이상 올라갈 곳이 없어요. 여기까지 못 찾으면 정말 없는 거예요!
중급
프로토타입 체인(Prototype Chain)은 JavaScript가 속성을 조회할 때 사용하는 메커니즘입니다. 객체에서 속성을 찾을 때 자신에게 없으면 __proto__ 링크를 따라 상위 프로토타입으로 탐색을 이어갑니다.
속성 탐색 알고리즘
- 현재 객체에서 속성 검색
- 발견되면 즉시 반환
- 없으면
__proto__를 따라 프로토타입 객체로 이동 - 프로토타입 객체에서 1-3 반복
__proto__가null이면undefined반환
function Animal(name) {
this.name = name;
}
Animal.prototype.eat = function() {
console.log(`${this.name} is eating`);
};
function Dog(name, breed) {
Animal.call(this, name);
this.breed = breed;
}
// Dog의 프로토타입을 Animal의 인스턴스로 설정
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
Dog.prototype.bark = function() {
console.log(`${this.name} says Woof!`);
};
const dog = new Dog('Buddy', 'Golden Retriever');
// 속성 탐색 과정
dog.bark(); // 1. dog에는 없음 → 2. Dog.prototype에서 발견 → 실행
dog.eat(); // 1. dog에는 없음 → 2. Dog.prototype에도 없음
// 3. Animal.prototype에서 발견 → 실행
console.log(dog.name); // 1. dog 자신에 있음 → 즉시 반환
// 체인 구조 확인
console.log(dog.__proto__ === Dog.prototype); // true
console.log(Dog.prototype.__proto__ === Animal.prototype); // true
console.log(Animal.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__ === null); // true
function Parent() {}
Parent.prototype.value = 10;
const child = new Parent();
console.log(child.value); // 10 (프로토타입에서 찾음)
// 자식에 같은 이름 속성 추가
child.value = 20;
console.log(child.value); // 20 (자신의 속성이 프로토타입 속성을 가림)
// 프로토타입의 속성은 여전히 존재
console.log(child.__proto__.value); // 10
hasOwnProperty()와 in 연산자
hasOwnProperty(): 객체 자신의 속성만 확인 (프로토타입 체인 탐색 안 함)in연산자: 프로토타입 체인 전체를 탐색
function Person(name) {
this.name = name;
}
Person.prototype.species = 'Human';
const person = new Person('Alice');
console.log('name' in person); // true
console.log('species' in person); // true
console.log(person.hasOwnProperty('name')); // true
console.log(person.hasOwnProperty('species')); // false
심화
프로토타입 체인 탐색은 ECMAScript 명세의 [[Get]] 내부 메서드와 속성 디스크립터(Property Descriptor) 시스템을 통해 정교하게 정의됩니다.
[[Get]] 내부 메서드의 명세 분석
ECMAScript 2023, Section 10.1.8 ([[Get]] Internal Method)의 추상 연산:
O.[[Get]](P, Receiver)
1. Let desc be ? O.[[GetOwnProperty]](P).
2. If desc is undefined, then
a. Let parent be ? O.[[GetPrototypeOf]]().
b. If parent is null, return undefined.
c. Return ? parent.[[Get]](P, Receiver). // 재귀적 탐색
3. If IsDataDescriptor(desc) is true, return desc.[[Value]].
4. Assert: IsAccessorDescriptor(desc) is true.
5. Let getter be desc.[[Get]].
6. If getter is undefined, return undefined.
7. Return ? Call(getter, Receiver).
2.c 단계의 재귀 호출이 프로토타입 체인 탐색의 핵심입니다. [[GetPrototypeOf]]는 내부 슬롯 [[Prototype]] (즉, __proto__)을 반환합니다.
속성 디스크립터와 체인 탐색
각 속성은 데이터 디스크립터(Data Descriptor) 또는 접근자 디스크립터(Accessor Descriptor)를 가지며, [[Get]]은 이를 구분하여 처리합니다:
- 데이터 디스크립터:
[[Value]]직접 반환 - 접근자 디스크립터: Getter 함수 호출 (
this는Receiver로 바인딩)
이는 프로토타입의 getter가 인스턴스에서 호출될 때도 올바른 this 바인딩을 보장합니다.
V8 엔진의 Inline Cache 최적화 V8은 프로토타입 체인 탐색을 Inline Cache(IC)로 극단적으로 최적화합니다:
-
Monomorphic IC: 속성이 항상 같은 위치(동일 Hidden Class)에 있으면, 체인 탐색 없이 직접 오프셋 접근으로 O(1) 성능을 달성합니다.
-
Prototype Chain Validity Cell: V8은 각 프로토타입 객체에 “유효성 셀(Validity Cell)“을 부착합니다. 프로토타입이 변경되면 셀을 무효화하여 IC를 갱신합니다.
-
Load IC Handler: 프로토타입 체인 깊이가 일정하면, TurboFan은 전체 체인 탐색을 인라인화한 핸들러를 생성합니다. 예: “자신 확인 → 1단계 프로토타입 확인 → 2단계 프로토타입에서 로드” 과정이 단일 머신 코드로 컴파일됩니다.
성능 특성 분석 프로토타입 체인 깊이에 따른 성능:
- 깊이 0 (자신의 속성): ~0.3ns (L1 캐시, Monomorphic IC)
- 깊이 1-2 (직계 프로토타입): ~1ns (인라인 핸들러)
- 깊이 3-5: ~5-10ns (폴리모픽 IC, 조건부 점프)
- 깊이 6+: ~50ns+ (Megamorphic IC, 해시 테이블 탐색)
따라서 프로토타입 체인은 2-3 단계 이내로 유지하는 것이 권장됩니다 (Google V8 팀 권고사항).
Property Shadowing의 명세적 의미
속성 가림은 [[GetOwnProperty]]가 먼저 실행되어 발견되면 즉시 반환하는 1번 단계의 동작입니다. 이는 다형성(Polymorphism)의 기초로, 자식 클래스가 부모 메서드를 오버라이드하는 메커니즘을 제공합니다.
ECMAScript 클래스 구문에서 메서드 오버라이딩도 내부적으로 프로토타입 체인 가림으로 구현됩니다:
class Animal {
speak() { return 'sound'; }
}
class Dog extends Animal {
speak() { return 'woof'; } // Animal.prototype.speak을 가림
}
Dog.prototype.speak이 Animal.prototype.speak보다 먼저 발견되므로, 프로토타입 체인 탐색은 첫 번째 단계에서 종료됩니다.
표준 메서드와 레거시 __proto__
입문
__proto__는 오래된 방법이라 요즘엔 다른 더 좋은 방법을 사용해요. 마치 옛날 전화기 대신 스마트폰을 쓰는 것처럼요!
📱 더 좋은 방법이 생겼어요
옛날에는 __proto__를 직접 만지는 게 유일한 방법이었어요. 하지만 이게 문제를 일으킬 수 있어서, JavaScript는 더 안전하고 좋은 방법들을 만들었어요. Object.getPrototypeOf()와 Object.setPrototypeOf()가 바로 그 방법들이에요.
🚨 왜 __proto__를 직접 쓰면 안 되나요?
__proto__를 직접 바꾸는 건 자동차 엔진을 달리는 중에 바꾸는 것과 비슷해요. 작동은 하지만 아주 위험하고 차가 고장날 수 있어요. JavaScript 엔진도 __proto__를 바꾸면 내부적으로 많은 최적화를 다시 해야 해서 느려져요.
✅ 어떤 방법을 써야 하나요?
부모가 누구인지 알고 싶을 때: Object.getPrototypeOf(객체)를 써요. 이건 안전하게 부모를 확인하는 방법이에요.
부모를 바꾸고 싶을 때: Object.setPrototypeOf(객체, 새부모)를 써요. 하지만 이것도 가급적 안 쓰는 게 좋아요. 객체를 만들 때 처음부터 올바른 부모를 설정하는 게 최선이에요.
🎯 처음부터 제대로 만들기
Object.create(부모)를 쓰면 처음부터 원하는 부모를 가진 객체를 만들 수 있어요. 나중에 바꾸는 것보다 훨씬 안전하고 빨라요!
중급
__proto__는 ECMAScript의 레거시 기능으로, Annex B에 정의되어 있으며 웹 호환성을 위해 유지됩니다. 현대 코드에서는 표준 메서드를 사용하는 것이 권장됩니다.
표준 메서드
Object.getPrototypeOf(obj): 객체의 프로토타입을 읽기 (권장)Object.setPrototypeOf(obj, proto): 객체의 프로토타입을 설정 (비권장, 성능 문제)Object.create(proto): 특정 프로토타입을 가진 새 객체 생성 (권장)
__proto__의 문제점
- 성능: 프로토타입 변경 시 엔진 최적화 무효화
- 보안: 프로토타입 오염(Prototype Pollution) 공격 가능
- 이식성: 모든 환경에서 지원되지 않을 수 있음
function Person(name) {
this.name = name;
}
Person.prototype.greet = function() {
return `Hello, ${this.name}`;
};
const user = new Person('Alice');
// ❌ 레거시 방법 (사용 지양)
console.log(user.__proto__ === Person.prototype); // true
user.__proto__ = { custom: 'object' }; // 위험!
// ✅ 표준 방법 (권장)
console.log(Object.getPrototypeOf(user) === Person.prototype); // true
// 프로토타입 변경은 가급적 피하되, 필요하면 표준 메서드 사용
Object.setPrototypeOf(user, { custom: 'object' }); // 명시적이지만 여전히 느림
const personMethods = {
greet() {
return `Hello, ${this.name}`;
},
introduce() {
return `I'm ${this.name}`;
}
};
// ✅ 생성 시점에 프로토타입 지정 (가장 권장)
const user = Object.create(personMethods);
user.name = 'Alice';
console.log(Object.getPrototypeOf(user) === personMethods); // true
console.log(user.greet()); // "Hello, Alice"
언제 무엇을 사용할까?
- 프로토타입 읽기:
Object.getPrototypeOf()(항상 안전) - 새 객체 생성:
Object.create()(생성 시점에 프로토타입 지정) - 프로토타입 변경: 가급적 피하고, 꼭 필요하면
Object.setPrototypeOf()(성능 비용 인지) __proto__직접 사용: 레거시 코드 유지보수 외에는 사용 금지
심화
__proto__는 ECMAScript Annex B.2.2 (Additional Properties of the Object.prototype Object)에 정의된 레거시 기능으로, 명세상 “웹 브라우저 호환성을 위한 비표준 기능”으로 분류됩니다.
__proto__ 접근자 프로퍼티의 명세 정의
Annex B.2.2.1에 따르면, Object.prototype.__proto__는 getter/setter 접근자 프로퍼티로 구현됩니다:
get __proto__()
1. Let O be ? ToObject(this value).
2. Return ? O.[[GetPrototypeOf]]().
set __proto__(proto)
1. Let O be ? RequireObjectCoercible(this value).
2. If Type(proto) is neither Object nor Null, return undefined.
3. If Type(O) is not Object, return undefined.
4. Let status be ? O.[[SetPrototypeOf]](proto).
5. If status is false, throw a TypeError.
6. Return undefined.
Setter는 내부 메서드 [[SetPrototypeOf]]를 호출하며, 이는 객체의 [[Prototype]] 내부 슬롯을 변경합니다.
표준 메서드의 명세 정의
Object.getPrototypeOf()와 Object.setPrototypeOf()는 Section 20.1.2에 정의되며, 동일한 내부 메서드를 호출하지만 더 명시적이고 안전합니다:
Object.getPrototypeOf(O)
1. Let obj be ? ToObject(O).
2. Return ? obj.[[GetPrototypeOf]]().
Object.setPrototypeOf(O, proto)
1. Set O to ? RequireObjectCoercible(O).
2. If Type(proto) is neither Object nor Null, throw a TypeError.
3. If Type(O) is not Object, return O.
4. Let status be ? O.[[SetPrototypeOf]](proto).
5. If status is false, throw a TypeError.
6. Return O.
차이점: Object.setPrototypeOf()는 타입 검증이 더 엄격하며, 실패 시 예외를 던집니다.
V8 엔진의 성능 최적화와 Deoptimization
V8에서 [[SetPrototypeOf]] 호출은 심각한 성능 비용을 유발합니다:
-
Hidden Class 전환: 객체의 Hidden Class를 변경하여 기존 최적화된 속성 접근 경로를 무효화합니다.
-
Inline Cache 무효화: 해당 객체에 대한 모든 Inline Cache가 무효화되어, 다음 접근부터 느린 경로(Slow Path)를 사용합니다.
-
Prototype Chain Validity Cell 무효화: 프로토타입이 변경되면 의존하는 모든 Inline Cache의 유효성 셀이 무효화됩니다.
-
TurboFan Deoptimization: 최적화된 함수가 해당 객체를 사용 중이면, 전체 함수가 비최적화 코드로 전환됩니다.
벤치마크 결과 (V8 11.0, 단일 객체 프로토타입 변경):
- 변경 전 속성 접근: 0.3ns (Monomorphic IC)
- 변경 후 첫 접근: 50ns (IC 재구축)
- 이후 접근: 5ns (Polymorphic IC로 안정화)
프로토타입 오염(Prototype Pollution) 보안 이슈
__proto__의 직접 사용은 보안 취약점을 유발할 수 있습니다. 특히 사용자 입력을 객체에 병합할 때:
// 취약한 코드
function merge(target, source) {
for (let key in source) {
target[key] = source[key]; // __proto__도 설정됨!
}
}
const userInput = JSON.parse('{"__proto__": {"isAdmin": true}}');
const user = {};
merge(user, userInput);
console.log({}.isAdmin); // true - 모든 객체가 오염됨!
방어책:
Object.create(null)로 프로토타입 없는 객체 사용Object.hasOwnProperty.call(source, key)로 검증Object.freeze(Object.prototype)로 프로토타입 수정 방지
Object.create()의 구현 최적화
Object.create(proto)는 내부적으로 OrdinaryObjectCreate(proto)를 직접 호출하므로, 생성 후 [[SetPrototypeOf]] 호출이 없습니다. 이는 Hidden Class를 안정적으로 유지하여 최적화에 유리합니다:
// Object.create() 사용 - Hidden Class 안정적
const obj1 = Object.create(proto); // 초기 Hidden Class 유지
// new + setPrototypeOf - Hidden Class 전환 발생
const obj2 = new Object();
Object.setPrototypeOf(obj2, proto); // Hidden Class 전환
성능 차이: Object.create()가 약 10배 빠름 (V8 11.0, n=1,000,000).
Non-Extensible 객체와 [[SetPrototypeOf]]
ECMAScript 명세 10.1.7.3에 따르면, 확장 불가능한 객체의 프로토타입은 변경할 수 없습니다:
const obj = Object.preventExtensions({});
Object.setPrototypeOf(obj, {}); // TypeError: Cannot set prototype
이는 객체의 불변성을 보장하는 보안 메커니즘입니다.