티스토리 뷰
[챗지피티와 공부를 해보자] 프로토타입 체인
프로토타입 (Prototype)
자바스크립트는 클래스 기반 언어가 아니라 프로토타입 기반의 객체 지향 언어이다.
여기서 객체 지향 언어라는건 === 객체지향 프로그래밍(OOP, Object-Oriented Programming)으로 데이터(속성)과 행동(메서드)을 하나의 객체(Object)로 묶어 프로그래밍하는 방법이다.
객체
- 속성 (Property) - 객체의 상태를 나타냄 (ex: 이름, 나이, 색상등..)
- 메서드 (Method) - 객체가 수행할 수 있는 동작 (ex: 걷다, 뛰다, 날다등..)
모든 객체는 자신의 부모 역할을 하는 객체인 프로토타입(Prototype)을 참조한다.
모든 객체는 프로토타입 링크 [[Prototype]] (또는 __proto__)라는 숨겨진 링크를 가지고 있으며, 이 링크는 해당 객체의 부모 객체(프로토타입)를 참조한다.
만약 객체에서 원하는 속성(Property)나 메서드(Method)를 찾지 못하면 이 링크를 통해 부모 객체에서 찾게된다.
프로토타입 체인(Prototype Chain)
객체가 특정 속성이나 메서드를 찾기 위해 연결된 프로토타입 링크를 따라 위로 올라가며 검색하는 구조이다.
프로토타입 체인은 상속 구조를 단순화하지만, 깊어질수록 속성 탐색 속도가 느려지므로 성능저하가 발생할 수 있다.
또한 객체에서 프로퍼티를 직접 설정하면 체인을 따라 올라가지 않고 객체 자체에서 값을 찾게되어 프로토타입 체이닝이 안되니 인지하고 있어야한다.
프로토타입 체인은 아래와 같은 과정을 말한다.
- 객체에서 원하는 속성이나 메서드를 찾는다
- 찾는 속성이나 메서드가 해당 객체에 없다면, 해당 객체의 프로토타입에서 찾는다. (해당 객체의 부모 객체)
- 그래도 없다면, 프로토타입의 프로토타입에서 찾는다. (부모객체의 부모)
- 끝까지 찾지 못하면 undefined를 반환한다.
// 생성자 함수 정의
function Person(name) {
this.name = name;
}
// 프로토타입에 메서드 추가
Person.prototype.greet = function () {
console.log(`Hello, my name is ${this.name}`);
};
// 객체 생성
const person1 = new Person('Alice');
// 객체에서 메서드 호출
person1.greet(); // 👉 "Hello, my name is Alice"
// 프로토타입 체인 탐색
console.log(person1.hasOwnProperty('name')); // 👉 true (person1 객체 자체에 존재)
console.log(person1.hasOwnProperty('greet')); // 👉 false (프로토타입에서 찾음)
person1.greet() 실행 시, person1 객체에서 greet 메서드를 찾게되는데, 이때 person1에는 그러한 메서드가 없기 때문에 프로토타입 체이닝을 통해 Person.prototype에서 메서드를 찾게된다.
프로토타입 체인의 구조는 아래와 같다.
person1 --> Person.prototype --> Object.prototype --> null
- person1: Person 생성자 함수를 통해 생성한 인스턴스 객체
- Person.prototype: 생성자 함수의 프로토타입 객체
- Object.prototype: 모든 객체의 최상위 프로토타입
- 기본 메서드들을 제공한다. (ex: toString, hasOwnProperty 등..)
- null: 더이상의 프로토타입 체인이 없을 경우, 프로토타입 체인 끝.
생성자 함수 (Constructor Function)
new 키워드를 사용해 새로운 객체를 만들기 위해 사용되는 함수, 생성자 함수로 인식되기 위해서는 반드시 함수여야한다.
일반적으로 생성자 함수는 첫글자를 대문자로 작성하며 (ex: Grandparent, Person) 함수 내부에서 this 키워드는 새로 생성된 객체를 참초한다.
Grandparent는 생성자 함수이며, Parent는 인스턴스 객체이다.
function Grandparent() {
this.family = 'Grandparent Family';
}
const Parent = new Grandparent(); // 여기서 Parent는 인스턴스 객체
동작
- Grandparent 함수 실행 시, 내부적으로 새로운 객체가 생성
- 반환된 객체는 일반 객체(Object) 이며, 이 객체를 Parent 변수에 저장
- 이 시점에서 Parent는 객체(Object) 이지, 더 이상 함수(Function)가 아니다.
다단계 상속 구현 예시
// 생성자 함수 선언
function Grandparent() {
this.family = 'Grandparent Family';
}
function Parent() {
Grandparent.call(this); // Grandparent 속성 상속
}
// 프로토타입 체인 연결
Parent.prototype = Object.create(Grandparent.prototype);
Parent.prototype.constructor = Parent;
// 자식 생성자 함수
function Child() {
Parent.call(this); // Parent 속성 상속
}
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
// 인스턴스 생성
const childInstance = new Child();
console.log(childInstance.family); // 👉 'Grandparent Family'
Q. [[Prototype]] (또는 __proto__) 프로토타입 링크를 개발자가 직접 접근하는 경우가 있어?
A. 직접 접근할 수 없습니다.
[[Prototype]] 은 모든 자바스크립트 객체가 가지고 있는 내부 링크이다.
모든 자바스크립트 객체가 가지고 있는 이유는 객체가 생성될때 자동으로 설정되며, 상속 체인을 구성하기 위한 핵심 요소이기 때문이다.
이는 공식적인 내부 링크이기 때문에 자바스크립트 명세(ECMAScript)에만 정의된 개념이기 때문에 개발자가 직접 접근할 수 없다. 다만, 간접적으로 다루기 위한 표준 메서드와 비표준 접근 방식이 존재한다.
Object.setPrototypeOf() 나 __proto__를 사용한 동적 프로토타입 변경은 성능 저하를 유발할 수 있다.
왜? 성능 저하를 유발할까?
자바스크립트 엔진(예: Google V8)은 객체의 속성과 구조를 예측하고 최적화하기 위해 숨겨진 클래스(hidden classes) 와 인라인 캐싱(inline caching) 기법을 사용한다고 한다.
이때 객체가 생성될때마다 숨겨진 클래스를 생성하는데, 이 객체의 속성이 추가되거나 삭제/변경 될때에 새로운 숨겨진 클래스가 생성되게 된다.
또한 이렇게 되면서, 객체가 새롭게 생성되면 이전에 인라인 캐싱된 정보를 사용할 수 없게된다. (인라인 캐싱은 프로퍼티 접근 패턴 및 반복적인 프로퍼티 접근을 빠르게 처리하기위해 관련 정보를 캐싱)
런타임에서의 프로토타입 변경은 이러한 최적화를 무효화 시키게되므로, 탐색 비용이 증가하므로 성능에 영향을 주게된다.
따라서 MDN에서도 성능 저하를 유발할 수 있다는 경고를 하고 있으며, 실행 중 사용을 피하는 것이 좋다.
표준 접근 (메서드 Object.getPrototypeOf() 와 Object.setPrototypeOf())
공식적으로 제공하는 메서드
// 프로토타입 읽기
const parent = {
greeting: "Hello"
};
const child = Object.create(parent);
// 프로토타입 설정
console.log(Object.getPrototypeOf(child) === parent); // 👉 true
// 프로토타입 설정
const newParent = {
farewell: "Goodbye"
};
Object.setPrototypeOf(child, newParent);
// 프로토타입 변경
console.log(child.farewell); // 👉 "Goodbye"
비표준 접근 (__proto__)
비표준 기능으로 사용이 권장되지 않으며, 일부 환경에서 지원하지 않을수 있다.
const parent = {
greeting: "Hello"
};
const child = {};
child.__proto__ = parent; // 부모 객체 연결 (수정)
console.log(child.greeting); // 👉 "Hello" (프로토타입에서 상속)
console.log(child.__proto__ === parent); // 👉 true
[[Prototype]] 접근 방법에 따른 권한 여부
방법 | 읽기 | 쓰기 | 표준 여부 | 비고 |
[[Prototype]] | ❌ | ❌ | ✅ | 내부 슬롯, 직접 접근 불가 |
__proto__ | ✅ | ✅ | ❌ | 비표준, 호환성 유지용 사용 가능 |
Object.getPrototypeOf() | ✅ | ❌ | ✅ | 표준, 안전한 프로토타입 접근 메서드 |
Object.setPrototypeOf() | ❌ | ✅ | ✅ | 표준, 프로토타입 수정 메서드 (성능 저하 주의) |
Q. 모든 자바스크립트 객체가 가지게 되는 이유?
A. 자바스크립트에서 객체는 두 가지 방법으로 생성되는데, 아래와 같은 방식으로 생성되기에 무조건 프로토타입을 가지게 된다.
- 객체리터럴로 생성 ({} 사용)
- 이 객체는 내부적으로 Object.create(Object.prototype) 과 같이 처리되며 생성된 객체는 자동으로 Object.prototype을 프로토타입으로 상속받는다.
- new 키워드로 생성 (new Object() 사용)
- 새로운 객체가 생성되고 Object.prototype을 프로토타입으로 가지게된다.
또한, 배열도 프로토 타입을 가지게되는데 배열 역시 본질적으로는 객체이며, 배열은 추가적으로 Array.prototype의 메서드와 속성을 상속 받는다.
arr --> Array.prototype --> Object.prototype --> null
Q. new 키워드 동작 원리
A. 아래와 같은 실행 과정을 거친다.
- new 키워드 사용
- 새로운 빈 객체를 생성한다. 내부적으로는 this = {} 를 생성한다.
- 생성된 객체는 [[Prototype]]을 생성자 함수의 prototype 객체로 설정한다.
- 생성자 함수의 코드를 실행하고, this를 해당 객체(new로 생성한)로 바인딩한다.
- 명시적으로 객체를 반환하지 않으면 새로 생성된 객체를 반환한다.
예시
function Grandparent() {
this.family = 'Grandparent Family';
}
const Parent = new Grandparent();
console.log(Parent); // 👉 { family: "Grandparent Family" }
console.log(Parent.__proto__ === Grandparent.prototype); // 👉 true
console.log(Grandparent.prototype.__proto__ === Object.prototype); // 👉 true
- new Grandparent() 실행 시, 새로운 빈 객체가 생성(this = {})되고 해당 객체는 Parent 변수에 할당된다.
- 이 객체는 Grandparent.prototype을 상속 받는다. (즉 내부적으로 this.__proto__ = Grandparent.prototype이 설정)
- Grandparent 함수가 실행되면서 해당 객체에 family 속성이 추가된다. (실행되는 코드는 this.family = 'Grandparent Family')
- 명시적으로 다른 값을 반환하지 않으면, 생성된 객체가 자동으로 반환된다. (return this가 암묵적으로 발생)
프로토타입 체인
Parent --> Grandparent.prototype --> Object.prototype --> null
Q. 생성자 함수로 새로운 인스턴스 생성이 가능할까?
A. 에러가 발생한다.
생성자가 아닌 일반 객체를 생성자처럼 호출하면 에러가 발생한다.
function Grandparent() {
this.family = 'Grandparent Family';
}
const Parent = new Grandparent(); // ✅ 이 시점에서 Parent는 일반 객체가 됨
const Child = new Parent(); // ❌ 에러 발생!
- Grandparent는 생성자 함수이다
- new 키워드 사용 시 새로운 객체가 생성되고, Grandparent.prototype을 상속받게 된다.
- 새로운 객체는 (=Parent) 일반 객체가 된다.
- Parent는 일반 객체이므로 new 키워드를 사용할 수 없다.
- new 키워드는 함수 또는 클래스에서만 사용할 수 있다. 따라서 Parent는 일반 객체이므로 에러가 발생한다. (객체에는 [[Construct]] 내부 메서드가 없으므로 생성자로 사용할 수 없다)
- TypeError: Parent is not a constructor
Q. [[Construct]] 내부 메서드에 대해 설명해줘
A. 자바스크립트에서 객체가 생성자로 호출될 수 있는지 여부를 결정하는 내부 메서드이다. (new 키워드를 통해 인스턴스를 생성할 수 있는지 판단)
자바스크립트는 함수객체 만이 [[Construct]] 내부 메서드를 가질수 있다. (=즉 함수만이 생성자로 동작 가능)
자바스크립트 엔진은 객체가 생성자 함수인지 판단할때,
- 해당 객체가 함수인지
- 해당 함수가 [[Constructor]] 내부 메서드가 있는지
두 조건이 충족될때, new 키워드를 통해 인스턴스를 생성할 수 있다.
객체가 생성자 함수인지 확인을 위한 공식 메서드는 없으나 Reflect.construct 또는 instanceof를 통해 확인할 수 있다.
function checkIfConstructor(obj) {
try {
Reflect.construct(String, [], obj);
return true;
} catch (e) {
return false;
}
}
console.log(checkIfConstructor(function () {})); // 👉 true
console.log(checkIfConstructor({})); // 👉 false
Q. 여기서 만약에 function Grandparent() { this.family = 'Grandparent Family'; return () => { // } } const Parent = new Grandparent(); 이런식으로 Grandparent를 return을 통해 함수를 반환시켜도 new 키워드를 통해 생성한 parent는 여전히 객체야?
A. 아니다. 함수(Function)를 반환했으므로, 생성된 객체 대신 반환된 함수가 할당된다.
자바스크립트에서는 생성자 함수에서 객체(Object)나 함수(Function)를 명시적으로 반환하면, 해당 반환값이 인스턴스 대신 사용할 수 있다.
function Grandparent() {
this.family = 'Grandparent Family';
return () => {
console.log('I am a returned function!');
};
}
const Parent = new Grandparent();
console.log(typeof Parent); // 👉 "function"
Parent(); // 👉 "I am a returned function!"
return 값이 Parent에 할당된다.
Q. return으로 함수를 반환할경우 생성자 함수는 this를 반환한 객체여야하는데, 이게 안되니까 그럼 Parent는 일반 함수가 되고 이렇게 되면 프로토타입 체인이 Grandparent를 바라보지 않겠네? 상속된 개념이 안되는거네? (실질적으로 new 키워드가 의미 없어지나?)
A. 정확히 맞다.
자바스크립트에서 생성자 함수가 객체(Object) 또는 함수(Function) 를 명시적으로 반환하면, this로 생성된 인스턴스를 무시하고 반환된 값을 대신 사용한다.
즉, new 키워드는 의미를 잃고 프로토타입 체인이 깨진다.
function Grandparent() {
this.family = 'Grandparent Family';
return () => {
console.log('Returned Function');
};
}
const Parent = new Grandparent();
Parent는 단순한 함수(Function)이며 더 이상 Grandparent.prototype 을 참조하지 않는다. (= 프로토타입 체인이 생성되지 않음)
new Grandparent() 실행 시 내부 동작 순서
- 새로운 빈 객체 {} 생성 (이 객체의 __proto__는 Grandparent.prototype을 참조)
- this.family 속성을 새 객체에 설정 → { family: "Grandparent Family" }
- 그러나 함수(() => {})를 명시적으로 반환 → 이 반환값이 인스턴스로 사용됨
- 반환된 함수가 객체 대신 반환되므로, this는 무시됨
프로토타입 체인이 깨지는 이유
자동 반환되는 this 객체는 무시되며, 반환된 값(객체/함수)이 그대로 인스턴스가 된다.
즉, Parent 객체는 Grandparent의 프로토타입을 상속받지 않으며 상속 개념이 무의미해지고, new 키워드의 의미도 사라진다.
왜? 반환된 함수는 Grandparent.prototype을 참조하지 않게되고, 따라서 프로토타입 체인은 당연히 연결되지 않는다.
반환된 값이 단순 함수(Function)로 간주되며, 오히려 Function.prototype 을 상속받게된다.
'개념 > AI와 함께' 카테고리의 다른 글
[챗지피티와 공부를 해보자] 가상돔(Virtual DOM)과 React Fiber 구조 (0) | 2025.03.26 |
---|---|
[챗지피티와 공부를 해보자] 이벤트 루프와 Web APIs의 관계 (0) | 2025.03.06 |
[챗지피티와 공부를 해보자] 이벤트 위임(Event Delegation)과 성능 최적화 (0) | 2025.02.27 |
[챗지피티와 공부를 해보자] 프로미스 체이닝 (Promise Chaining) (0) | 2025.02.12 |
[챗지피티와 공부를 해보자] 원시값(Primitive Value)과 참조값(Reference Value), 객체 복사(얕은 복사 Shallow Copy, 깊은 복사 Deep Copy) (0) | 2025.02.08 |
[챗지피티와 공부를 해보자] 이터러블(iterable)과 이터레이터(iterator) (0) | 2025.02.06 |
[챗지피티와 공부를 해보자] 구조분해할당 (Destructuring) (0) | 2025.02.03 |
[챗지피티와 공부를 해보자] 실행 컨텍스트(Execution Context) (2) | 2025.01.30 |