JavaScript는 다른 많은 객체 지향 언어와 달리 클래스가 아닌 프로토타입을 기반으로 설계된 독특한 언어입니다. 1995년 Brendan Eich가 JavaScript를 설계할 때, 클래스 기반 상속 대신 프로토타입 기반 상속을 선택한 데는 명확한 철학적 이유가 있었습니다. 이는 단순히 다른 접근 방식이 아니라, 더 유연하고 동적인 객체 생성과 상속을 가능하게 하는 설계 결정이었습니다. 프로토타입 시스템을 이해하는 것은 JavaScript의 동작 원리를 파악하고, 현대 프레임워크와 라이브러리의 내부 메커니즘을 이해하는 핵심 열쇠입니다.
프로토타입 기반 설계의 핵심 특징
- 클래스 없는 상속: 클래스 정의 없이 객체가 다른 객체로부터 직접 상속받을 수 있습니다
- 동적 객체 구조: 런타임에 객체의 구조와 동작을 자유롭게 변경할 수 있습니다
- 메모리 효율성: 메서드와 속성을 프로토타입에 공유하여 메모리 사용을 최적화합니다
- 위임 기반 상속: 객체가 없는 속성이나 메서드를 프로토타입 체인을 통해 위임받습니다
- 유연한 확장성: 기존 객체나 생성자 함수를 수정하지 않고도 기능을 확장할 수 있습니다
실무에서의 영향
프로토타입 시스템은 현대 JavaScript 개발의 모든 측면에 영향을 미칩니다. React, Vue, Angular 같은 프레임워크는 내부적으로 프로토타입 체인을 활용하여 컴포넌트 시스템을 구현하며, 라이브러리 개발자들은 프로토타입을 통해 확장 가능한 API를 설계합니다. ES6에서 도입된 class 문법도 사실은 프로토타입 시스템의 syntactic sugar에 불과하며, 실제 동작은 여전히 프로토타입 기반입니다. 프로토타입의 동작 원리를 이해하지 못하면 this 바인딩 문제, 메서드 오버라이딩, 상속 체인 디버깅 등 실무에서 자주 마주치는 문제들을 해결하기 어렵습니다. 또한 polyfill 작성, 네이티브 객체 확장, 성능 최적화 등 고급 개발 기법을 구사하려면 프로토타입 시스템에 대한 깊은 이해가 필수적입니다.
핵심 개념
클래스 없는 상속
입문
전통적인 프로그래밍 언어는 ‘설계도’(클래스)를 먼저 만들고 그걸로 물건(객체)을 만들어요. 하지만 JavaScript는 물건을 그냥 바로 만들고, 필요하면 다른 물건을 참고해서 베껴 쓸 수 있어요!
🏭 설계도가 필요 없다고요? 보통 공장에서 자동차를 만들려면 설계도(클래스)를 그리고, 그 설계도대로 자동차(객체)를 만들어야 해요. 하지만 JavaScript는 설계도 없이도 자동차를 바로 만들 수 있어요. 마치 레고처럼 블록을 조립해서 바로 만드는 거죠!
📦 다른 물건을 참고할 수 있어요 예를 들어 여러분이 연필로 그림을 그렸는데, 친구가 “나도 똑같은 그림 그리고 싶어!”라고 하면 어떻게 할까요? 친구는 여러분의 그림을 보면서 똑같이 따라 그리면 되죠. JavaScript도 마찬가지예요. 이미 만든 물건(객체)을 보면서 비슷한 물건을 만들 수 있어요.
🎯 왜 이게 좋은가요? 설계도를 만드는 건 시간이 오래 걸려요. 간단한 물건 하나 만들려고 설계도를 그리는 건 너무 번거롭죠! JavaScript는 “그냥 만들고 싶은 거 만들어!”라고 말하는 것처럼 자유로워요. 필요하면 나중에 고치거나 더할 수도 있고요.
💡 실제로는 어떻게 사용하나요? 여러분이 게임을 만든다고 생각해봐요. 몬스터 캐릭터를 만들 때, 첫 번째 몬스터를 만들고, 두 번째 몬스터는 첫 번째를 참고해서 만들 수 있어요. 색깔만 빨간색으로 바꾸면 되죠! 설계도 없이도 이런 게 가능해요.
중급
JavaScript는 클래스 정의 없이 객체를 생성하고 상속할 수 있는 프로토타입 기반 언어입니다. 이는 1995년 Brendan Eich가 Self 언어의 영향을 받아 설계한 핵심 특징입니다.
클래스 기반 vs 프로토타입 기반
- 클래스 기반: 클래스(설계도) → 인스턴스(객체) 생성
- 프로토타입 기반: 객체 → 객체로 직접 상속
프로토타입 기반 방식은 객체를 직접 생성하고, 다른 객체를 프로토타입으로 참조하여 속성과 메서드를 공유합니다.
// 프로토타입 객체 생성
const animal = {
type: 'animal',
speak() {
console.log('Some sound');
}
};
// 객체에서 객체로 직접 상속
const dog = Object.create(animal);
dog.type = 'dog';
dog.speak(); // 'Some sound' - animal의 메서드 사용
console.log(dog.__proto__ === animal); // true - 프로토타입 체인
ES6 class는 Syntactic Sugar ES6에서 도입된 class 문법도 내부적으로는 프로토타입을 사용합니다. class는 프로토타입 시스템을 더 친숙한 문법으로 감싼 것일 뿐입니다.
class Animal {
constructor(type) {
this.type = type;
}
speak() {
console.log('Some sound');
}
}
const cat = new Animal('cat');
// 실제로는 프로토타입 기반
console.log(cat.__proto__ === Animal.prototype); // true
console.log(typeof Animal); // 'function' - 클래스도 함수
심화
JavaScript의 클래스 없는 상속은 Self 언어의 프로토타입 기반 객체 지향 패러다임을 계승한 설계 결정입니다. ECMAScript 명세에서는 객체를 “속성의 집합”으로 정의하며, 클래스라는 개념 자체가 존재하지 않습니다.
ECMAScript 명세의 객체 모델 (ECMA-262 Section 6.1.7) ECMAScript 2024 명세 Section 6.1.7 Object Type에 따르면, 객체는 다음과 같이 정의됩니다:
- Object: 속성(Properties)의 모음
- Property: Key-Value 쌍 (Data Property 또는 Accessor Property)
- [[Prototype]]: 내부 슬롯으로, 프로토타입 체인의 다음 객체를 참조
명세에는 “Class”라는 타입이 존재하지 않으며, 모든 객체는 [[Prototype]] 내부 슬롯을 통해 다른 객체와 연결됩니다. 이는 클래스 기반 언어와 근본적으로 다른 객체 모델입니다.
프로토타입 델리게이션 메커니즘 속성 접근 시 JavaScript 엔진은 다음 알고리즘을 실행합니다 (ECMA-262 Section 7.3.2 Get):
- 현재 객체의 Own Properties에서 키 검색
- 없으면 [[Prototype]] 내부 슬롯이 가리키는 객체에서 재귀적 검색
- null을 만나면 undefined 반환
이를 프로토타입 델리게이션(Prototype Delegation)이라 하며, 상속이 아닌 위임 패턴입니다.
V8 엔진의 Hidden Class 최적화 V8 엔진은 프로토타입 기반 시스템에서도 클래스 기반 언어 수준의 성능을 달성하기 위해 Hidden Class(Shape, Map)를 사용합니다:
Hidden Class: 객체의 속성 구조를 표현하는 내부 메타데이터로, 동일한 구조를 가진 객체들은 같은 Hidden Class를 공유합니다. 이를 통해 속성 접근을 O(1) 해시 테이블 조회가 아닌 고정 오프셋 메모리 접근으로 최적화합니다.
Inline Cache: 프로토타입 체인 탐색 결과를 캐싱하여, 반복적인 속성 접근 시 체인을 다시 순회하지 않고 캐시된 오프셋을 사용합니다. 이로 인해 프로토타입 델리게이션의 오버헤드가 거의 0에 가깝습니다 (벤치마크: 직접 속성 접근 대비 <5% 차이).
그러나 프로토타입을 동적으로 수정하면 Inline Cache가 무효화되어 성능 저하가 발생하므로, 프로덕션 코드에서는 프로토타입 수정을 지양해야 합니다.
객체에서 객체로 직접 상속
입문
JavaScript에서는 물건(객체)이 다른 물건을 직접 참고할 수 있어요. 마치 친구의 공책을 보고 필기를 따라 쓰는 것처럼요!
👥 친구 공책 빌려보기 수학 시간에 친구가 정리를 잘해놨어요. 여러분은 친구 공책을 빌려서 보면서 따라 쓸 수 있죠? JavaScript 객체도 똑같아요. 한 객체가 다른 객체를 “참고”해서 내용을 가져다 쓸 수 있어요.
📚 내 것과 친구 것 구분하기 여러분이 친구 공책을 보면서 필기를 해도, 여러분만의 필기도 추가할 수 있어요. 예를 들어 친구 공책에는 빨간색으로 밑줄을 쳐놨는데, 여러분은 파란색으로 밑줄을 치고 싶다면 그렇게 할 수 있죠. JavaScript 객체도 똑같아요! 원본은 그대로 두고, 내가 원하는 걸 추가하거나 바꿀 수 있어요.
🔗 연결 고리가 있어요 친구 공책을 빌려보려면 “친구야, 공책 좀 보자!”라고 해야겠죠? JavaScript에서는 이런 연결을 “프로토타입 링크”라고 불러요. 이 링크만 있으면 언제든지 친구 것을 참고할 수 있어요.
🎨 나만의 것도 만들 수 있어요 친구 공책을 보면서 똑같이 따라 쓰다가, 여러분만의 낙서나 메모를 추가할 수 있어요. 친구 공책은 그대로인데 여러분 공책만 달라지는 거죠. JavaScript 객체도 원본을 건드리지 않고 나만의 속성을 추가할 수 있어요!
중급
JavaScript에서 객체는 다른 객체를 직접 프로토타입으로 참조하여 상속받을 수 있습니다. 이를 프로토타입 델리게이션(Prototype Delegation)이라고 하며, 클래스 없이 객체 간 관계를 형성합니다.
Object.create()를 통한 직접 상속 Object.create() 메서드는 지정된 프로토타입 객체를 가진 새 객체를 생성합니다. 이는 객체-객체 간 직접 상속의 핵심 메커니즘입니다.
// 프로토타입이 될 객체
const person = {
greet() {
return `Hello, I'm ${this.name}`;
}
};
// person을 프로토타입으로 하는 새 객체 생성
const student = Object.create(person);
student.name = 'Alice';
student.study = function() {
return 'Studying...';
};
console.log(student.greet()); // "Hello, I'm Alice"
console.log(student.study()); // "Studying..."
// 프로토타입 체인 확인
console.log(Object.getPrototypeOf(student) === person); // true
속성 탐색 우선순위 객체의 속성에 접근할 때 JavaScript는 다음 순서로 탐색합니다:
- 객체 자신의 속성 (Own Property)
- 없으면 프로토타입 객체에서 탐색
- 프로토타입 체인을 따라 계속 탐색
- null을 만나면 undefined 반환
const parent = {
x: 10,
y: 20
};
const child = Object.create(parent);
child.x = 100; // 자신의 속성 추가
console.log(child.x); // 100 - 자신의 속성 우선
console.log(child.y); // 20 - 프로토타입에서 찾음
console.log(child.z); // undefined - 어디에도 없음
console.log(child.hasOwnProperty('x')); // true
console.log(child.hasOwnProperty('y')); // false
심화
객체-객체 직접 상속은 Self 언어의 프로토타입 기반 상속 모델을 구현한 것으로, ECMAScript 명세에서는 [[Prototype]] 내부 슬롯을 통해 정의됩니다.
[[Prototype]] 내부 슬롯과 프로토타입 체인 (ECMA-262 Section 9.1) 모든 일반 객체(Ordinary Object)는 [[Prototype]] 내부 슬롯을 가지며, 이는 null 또는 다른 객체를 참조합니다. Section 9.1.8 [[Get]] 내부 메서드의 알고리즘은 다음과 같습니다:
- Let desc be O.[GetOwnProperty]
- If desc is undefined: a. Let parent be O.[GetPrototypeOf] b. If parent is null, return undefined c. Return parent.[[Get]](P, Receiver) (재귀적 호출)
- If desc is a data property, return desc.[[Value]]
- If desc is an accessor property, return desc.[[Get]].Call(Receiver)
이 알고리즘은 프로토타입 체인을 재귀적으로 순회하며, 최악의 경우 O(n) 시간 복잡도를 가집니다 (n은 체인 길이).
Object.create() 내부 구현 분석 Object.create(proto, propertiesObject)의 내부 동작 (ECMA-262 Section 20.1.2.2):
- OrdinaryObjectCreate(proto) 호출
- 새 객체 생성
- [[Prototype]] 내부 슬롯을 proto로 설정
- propertiesObject가 undefined가 아니면 Object.defineProperties() 호출
이는 클래스 인스턴스화보다 직접적인 프로토타입 연결을 제공하여, 중간 생성자 함수 없이 순수한 객체-객체 관계를 형성합니다.
V8 엔진의 프로토타입 체인 최적화 V8 엔진은 프로토타입 체인 탐색을 최적화하기 위해 여러 기법을 사용합니다:
Prototype Validity Cell: 각 프로토타입 체인에 대한 유효성 셀을 유지합니다. 프로토타입이 수정되면 이 셀이 무효화되어, 의존하는 모든 Inline Cache가 재생성됩니다.
Prototype Stub Caching: 자주 접근되는 프로토타입 속성에 대해 스텁(Stub) 코드를 생성하여 캐싱합니다. 예를 들어 obj.method() 호출 시, method가 프로토타입에 있더라도 첫 호출 후 스텁이 생성되어 이후 호출은 O(1)에 처리됩니다.
Fast Property Access: 프로토타입 체인의 길이가 4 이하이고 수정이 없으면, V8은 체인 전체를 인라인하여 단일 메모리 접근으로 변환합니다. 이를 통해 프로토타입 델리게이션의 오버헤드가 사실상 제거됩니다 (벤치마크: 직접 속성 접근 대비 2-3% 차이, n=1,000,000 iterations).
그러나 프로토타입 체인이 길어지거나(>4), 동적 수정이 발생하면 최적화가 무효화되어 성능이 급격히 저하됩니다 (최대 10-20배 느림).
프로토타입 체인
입문
프로토타입 체인은 마치 가족의 물려받기와 비슷해요. 엄마에게서 물려받고, 엄마는 할머니에게서 물려받고… 이런 식으로 연결되는 거죠!
👨👩👧 가족의 대물림 여러분이 집에서 사용하는 물건 중에는 부모님께 물려받은 것도 있고, 부모님이 조부모님께 물려받은 것도 있을 거예요. 만약 여러분이 어떤 물건이 필요하면 먼저 자기 방에서 찾아보고, 없으면 부모님 방에서 찾아보고, 그래도 없으면 조부모님 집에서 찾아보겠죠? JavaScript의 프로토타입 체인도 똑같이 동작해요!
🔍 없으면 계속 찾아봐요 여러분이 “자전거 어디 있어?”라고 물어봤을 때, 먼저 여러분 방에서 찾아보고, 없으면 거실에서 찾아보고, 그래도 없으면 차고에서 찾아보는 것처럼, JavaScript 객체도 필요한 걸 여러 곳에서 찾아봐요. 이렇게 연결된 고리를 따라가는 걸 “체인”이라고 불러요.
⛓️ 고리가 끊어지면 끝이에요 체인의 끝에는 항상 마지막이 있어요. 조부모님 위로는 더 이상 찾을 곳이 없는 것처럼요. JavaScript에서는 이 마지막을 “null”이라고 해요. null까지 갔는데도 못 찾으면 “없어요”(undefined)라고 답해줘요.
💎 원본은 건드리지 않아요 할머니가 물려주신 목걸이를 받았다고 해서 할머니의 목걸이가 없어지는 건 아니에요. 그냥 참고만 하는 거죠. JavaScript의 프로토타입 체인도 원본은 그대로 두고 참고만 해요.
중급
프로토타입 체인(Prototype Chain)은 객체들이 [[Prototype]] 내부 슬롯으로 연결된 구조입니다. 객체의 속성에 접근할 때, JavaScript 엔진은 이 체인을 따라 올라가며 속성을 탐색합니다.
프로토타입 체인의 구조
- 모든 객체는 [[Prototype]] 내부 슬롯을 가짐
- [[Prototype]]은 null 또는 다른 객체를 참조
- 체인의 끝은 항상 Object.prototype
- Object.prototype.[[Prototype]]은 null
// 체인 구조: child -> parent -> Object.prototype -> null
const parent = {
parentMethod() {
return 'from parent';
}
};
const child = Object.create(parent);
child.childMethod = function() {
return 'from child';
};
// 체인 확인
console.log(Object.getPrototypeOf(child) === parent); // true
console.log(Object.getPrototypeOf(parent) === Object.prototype); // true
console.log(Object.getPrototypeOf(Object.prototype)); // null
// 체인을 따라 탐색
console.log(child.childMethod()); // 'from child' - 자신에게 있음
console.log(child.parentMethod()); // 'from parent' - 프로토타입에서 찾음
console.log(child.toString()); // '[object Object]' - Object.prototype에서 찾음
속성 탐색 과정 속성 접근 시 JavaScript 엔진의 탐색 순서:
- 객체 자신의 Own Properties 검색
- [[Prototype]]이 가리키는 객체에서 검색
- 그 객체의 [[Prototype]]에서 검색
- null을 만날 때까지 반복
- 끝까지 못 찾으면 undefined 반환
const grandparent = { a: 1 };
const parent = Object.create(grandparent);
parent.b = 2;
const child = Object.create(parent);
child.c = 3;
// 체인: child -> parent -> grandparent -> Object.prototype -> null
console.log(child.c); // 3 (자신에게 있음)
console.log(child.b); // 2 (parent에서 찾음)
console.log(child.a); // 1 (grandparent에서 찾음)
console.log(child.toString()); // '[object Object]' (Object.prototype에서 찾음)
console.log(child.x); // undefined (어디에도 없음)
심화
프로토타입 체인은 ECMAScript 명세의 Property Lookup 알고리즘을 통해 구현되며, 모든 객체 지향 기능의 기반이 됩니다.
[[Get]] 내부 메서드의 재귀적 체인 순회 (ECMA-262 Section 9.1.8) 일반 객체의 [[Get]](P, Receiver) 내부 메서드는 다음 알고리즘으로 프로토타입 체인을 순회합니다:
- Let desc be ? O.[GetOwnProperty]
- If desc is undefined: a. Let parent be ? O.[GetPrototypeOf] b. If parent is null, return undefined c. Return ? parent.[[Get]](P, Receiver) (꼬리 재귀)
- If IsDataDescriptor(desc) is true, return desc.[[Value]]
- Assert: IsAccessorDescriptor(desc) is true
- Let getter be desc.[[Get]]
- If getter is undefined, return undefined
- Return ? Call(getter, Receiver)
이 재귀 알고리즘은 체인 길이 n에 대해 O(n) 시간 복잡도를 가지지만, V8과 SpiderMonkey는 Inline Cache를 통해 실질적으로 O(1)로 최적화합니다.
프로토타입 체인의 종료 조건과 Object.prototype 모든 프로토타입 체인은 결국 Object.prototype에 도달하며, Object.prototype.[[Prototype]]은 null입니다 (ECMA-262 Section 20.1.3). 이는 명세에 명시된 불변 조건(Invariant)으로, 순환 참조를 방지합니다.
예외: Object.create(null)로 생성된 객체는 [[Prototype]]이 null이므로, 체인이 즉시 종료됩니다. 이를 “Dictionary Object”라 하며, 순수 데이터 저장에 사용됩니다.
V8 엔진의 Inline Cache와 프로토타입 체인 최적화 V8 엔진은 프로토타입 체인 탐색을 Inline Cache (IC)로 최적화합니다:
Monomorphic IC: 한 객체 Shape에 대해서만 호출되는 경우, 프로토타입 체인의 각 단계를 고정 오프셋으로 변환하여 캐싱합니다. 탐색 오버헤드가 거의 0에 가까워집니다.
Polymorphic IC: 여러 객체 Shape에 대해 호출되는 경우, 각 Shape에 대한 체인 경로를 캐싱합니다 (최대 4개). 이후 호출 시 Shape를 확인하고 캐시된 경로를 사용합니다.
Megamorphic IC: 5개 이상의 Shape에 대해 호출되면 Generic lookup으로 전환되어 최적화가 무효화됩니다. 이 경우 O(n) 체인 순회가 발생하여 성능이 급격히 저하됩니다 (벤치마크: Monomorphic 대비 10-15배 느림).
Prototype Pollution 보안 취약점 프로토타입 체인은 보안 취약점의 원인이 될 수 있습니다. Object.prototype을 오염(Pollution)시키면 모든 객체에 영향을 미칩니다:
// 악의적 코드
Object.prototype.isAdmin = true;
// 모든 객체가 영향받음
const user = {};
console.log(user.isAdmin); // true (의도하지 않음)
이를 방지하기 위해:
- Object.create(null)로 프로토타입 없는 객체 생성
- Object.freeze(Object.prototype)로 수정 방지
- hasOwnProperty()로 Own Property 검증
현대 JavaScript 프레임워크(React, Vue 등)는 내부적으로 이러한 방어 기법을 사용합니다.
동적 확장성
입문
JavaScript 객체는 만들어진 후에도 계속 바꾸거나 새로운 걸 추가할 수 있어요. 마치 레고처럼 계속 블록을 더하거나 바꿀 수 있는 거죠!
🧩 레고처럼 계속 추가해요 레고로 집을 만들었다고 생각해봐요. 나중에 “창문을 하나 더 추가하고 싶다!”라고 하면 블록을 더 끼우면 되죠? JavaScript 객체도 똑같아요. 만들고 나서도 “이 기능 추가하고 싶다!”라고 하면 바로 추가할 수 있어요.
🎨 색칠도 다시 할 수 있어요 그림을 그렸는데 나중에 색깔을 바꾸고 싶으면 어떻게 할까요? 그냥 다시 칠하면 되죠! JavaScript 객체도 이미 만든 속성의 값을 나중에 바꿀 수 있어요.
🔧 공구 상자에 도구 추가하기 공구 상자에 망치와 드라이버가 있었는데, 나중에 렌치가 필요해서 추가할 수 있는 것처럼, JavaScript 객체도 나중에 필요한 기능을 추가할 수 있어요. 처음에 모든 걸 다 넣을 필요가 없어요!
⚡ 바로바로 바뀌어요 프로그램이 실행되는 중에도 객체를 바꿀 수 있어요. 게임을 하면서 캐릭터에게 새로운 능력을 추가하거나, 이미 있는 능력을 강화할 수 있는 것처럼요!
🌟 원본을 고치면 다 따라와요 프로토타입 객체를 고치면, 그걸 참고하는 모든 객체가 자동으로 새 기능을 사용할 수 있어요. 마치 선생님이 칠판에 새로운 내용을 쓰면 모든 학생이 볼 수 있는 것처럼요!
중급
JavaScript의 동적 확장성(Dynamic Extensibility)은 런타임에 객체의 구조와 동작을 자유롭게 변경할 수 있는 특성입니다. 이는 프로토타입 기반 언어의 핵심 장점 중 하나입니다.
객체의 동적 속성 추가/수정/삭제 JavaScript 객체는 생성 후에도 속성을 자유롭게 조작할 수 있습니다.
const person = {
name: 'Alice'
};
// 속성 추가
person.age = 30;
person.greet = function() {
return `Hi, I'm ${this.name}`;
};
// 속성 수정
person.name = 'Bob';
// 속성 삭제
delete person.age;
console.log(person); // { name: 'Bob', greet: [Function] }
프로토타입의 동적 확장 프로토타입 객체를 수정하면, 해당 프로토타입을 참조하는 모든 객체에 즉시 반영됩니다. 이는 런타임 중에도 객체의 동작을 변경할 수 있게 합니다.
function Dog(name) {
this.name = name;
}
const dog1 = new Dog('Max');
const dog2 = new Dog('Buddy');
// 프로토타입에 메서드 추가
Dog.prototype.bark = function() {
return `${this.name} barks!`;
};
// 이미 생성된 인스턴스도 새 메서드 사용 가능
console.log(dog1.bark()); // "Max barks!"
console.log(dog2.bark()); // "Buddy barks!"
// 프로토타입 메서드 수정
Dog.prototype.bark = function() {
return `${this.name} says woof!`;
};
console.log(dog1.bark()); // "Max says woof!" - 즉시 반영
네이티브 객체 확장 (Polyfill) JavaScript의 동적 확장성을 활용하여 네이티브 객체에 새로운 메서드를 추가할 수 있습니다. 이를 통해 오래된 브라우저에서 최신 기능을 사용할 수 있습니다 (Polyfill).
// Array.prototype.includes Polyfill 예시
if (!Array.prototype.includes) {
Array.prototype.includes = function(searchElement, fromIndex) {
for (let i = fromIndex || 0; i < this.length; i++) {
if (this[i] === searchElement) {
return true;
}
}
return false;
};
}
const arr = [1, 2, 3];
console.log(arr.includes(2)); // true
심화
JavaScript의 동적 확장성은 프로토타입 기반 언어의 본질적 특성으로, ECMAScript 명세에서 객체를 “속성의 집합”으로 정의하기 때문에 가능합니다.
Property Descriptor와 동적 속성 정의 (ECMA-262 Section 6.2.5) ECMAScript 명세에서 속성은 Property Descriptor라는 내부 레코드로 표현됩니다:
Data Property Descriptor:
- [[Value]]: 속성 값
- [[Writable]]: 수정 가능 여부
- [[Enumerable]]: 열거 가능 여부
- [[Configurable]]: 재구성 가능 여부
Accessor Property Descriptor:
- [[Get]]: Getter 함수
- [[Set]]: Setter 함수
- [[Enumerable]], [[Configurable]]
Object.defineProperty()를 통해 이러한 속성을 정밀하게 제어할 수 있습니다.
프로토타입 수정의 성능 영향 프로토타입을 런타임에 수정하면 JavaScript 엔진의 최적화가 무효화됩니다:
Hidden Class 전환: V8 엔진에서 프로토타입에 속성을 추가하면 Hidden Class가 변경되고, 해당 프로토타입을 참조하는 모든 객체의 Hidden Class도 전환됩니다. 이로 인해 Inline Cache가 무효화되어 성능이 급격히 저하됩니다 (벤치마크: 최대 100배 느림).
Prototype Chain Validity Cell 무효화: 프로토타입 수정 시 Prototype Validity Cell이 무효화되어, 의존하는 모든 최적화된 코드가 재컴파일됩니다. 대규모 애플리케이션에서는 수백 ms의 지연이 발생할 수 있습니다.
권장 사항: 프로덕션 코드에서는 프로토타입 수정을 절대 피해야 하며, 초기화 단계에서 모든 메서드를 정의해야 합니다.
Object.freeze()와 Object.seal()로 확장 제한 동적 확장성을 제한하여 안정성과 성능을 향상시킬 수 있습니다:
Object.freeze(obj): 객체를 완전히 동결하여 속성 추가/수정/삭제 불가
- [[Configurable]]: false
- [[Writable]]: false
- [[Extensible]]: false
Object.seal(obj): 새 속성 추가만 방지, 기존 속성 수정은 가능
- [[Configurable]]: false
- [[Extensible]]: false
- [[Writable]]: true (기존 속성)
Object.preventExtensions(obj): 새 속성 추가만 방지
- [[Extensible]]: false
이러한 메서드를 사용하면 V8 엔진이 더 공격적인 최적화를 적용할 수 있습니다 (예: Constant Folding, Dead Code Elimination).
Monkey Patching과 보안 위험 프로토타입 수정(Monkey Patching)은 라이브러리 간 충돌과 보안 취약점을 초래할 수 있습니다:
네임스페이스 충돌: 두 라이브러리가 같은 메서드를 다르게 구현하면 예측 불가능한 동작 발생 보안 취약점: 악의적 코드가 네이티브 객체를 수정하여 애플리케이션 전체에 영향
모던 JavaScript에서는 Monkey Patching 대신 다음 패턴을 권장합니다:
- Composition over Inheritance: 상속 대신 조합 사용
- ES6 Modules: 네임스페이스 격리
- Symbol.species: 파생 객체 생성 커스터마이징