안녕하세요. 동쪽별입니다.
개발 관련 첫 포스팅으로 자바스크립트의 프로토타입에 대해 깊~게 살펴보려 해요.
자바스크립트에 대해 잘 모르시는 분들은 이해가 안되는 부분도 있을거에요. 최~대한 쉽게 읽힐 수 있도록 최선을 다해보겠습니다!
프로토타입은 자바스크립트의 상속을 지원하기 위한 방법입니다.
그런데, 왜 다른 언어처럼 클래스가 아니라 프로토타입일까요?
그 이유는 자바스크립트가 접근한 철학적 사고방식 때문입니다.
클래스 기반 객체지향 언어(Java, C# 등)은 "모든 것에는 반드시 본질이 존재한다" 라는 사고 방식이 녹아들어 있어요.
이에 반해, 자바스크립트의 프로토타입은 "모든 것은 분류 되는 것이 아니라, 가장 좋은 보기로부터 범주화된다" 라는 사고방식으로 생성되었어요.
즉, 프로토타입은 가장 좋은 본보기다! 라고 할 수 있겠네요.
그러므로 프로토타입 체계에서 ‘상속’, '인스턴스' 라는 표현은 적절하지 않을 수 있어요.
하지만 더 나은 이해를 위해서 해당 단어들을 사용할게요.
철학적인 이야기가 갑자기 나와 당황스러울 수 있지만, 해당 내용에 관한 글을 읽으면 꽤 재미있으실 거에요 😊
자, 그럼 본격적으로 시작해 볼게요.
목차
- 상속
- __proto__ 접근자 프로퍼티
- prototype 프로퍼티
- constructor 프로퍼티
- 프로토타입 체인
상속
상속! 객체지향 프로그래밍의 🌸 이라 할 수 있죠.
상속이란 어떤 객체의 프로퍼티 또는 메서드를 다른 객체가 상속 받아 그대로 사용할 수 있는 것을 말해요.
자바스크립트는 프로토타입 기반 객체지향 프로그래밍 언어로 프로토타입을 기반으로 상속을 구현하고, 이를 통해 불필요한 중복을 제거합니다.
먼저, 프로토타입을 이해하기 위해 생성자 함수에 대해 간단히 살펴볼게요.
생성자 함수란, 일반 함수와 기술적인 차이는 없지만
- 반드시 new 연산자를 붙여 실행
- 함수 이름의 첫 글자를 대문자로 시작
이라는 두 관례를 따릅니다.
이러한 생성자 함수를 이용하면 동일한 프로퍼티, 메서드 구조를 갖는 객체 여러 개를 쉽게 생성할 수 있어요!
function Friend(name) {
this.name = name;
this.introduce = () => {
return `안녕하세요. ${this.name}입니다!`
}
}
const friend1 = new Friend('짱구');
const friend2 = new Friend('철수');
console.log(friend1.introduce === friend2.introduce);
// false
생성자 함수를 통해 생성한 두 객체 friend1, friend2 가 가지는 메서드 introduce 가 동일하지 않네요..?
왜냐하면! Friend 생성자 함수는 인스턴스를 생성할 때마다 introduce 메서드를 중복 생성하기 때문에!
❗️ 즉, 모든 인스턴스가 메서드를 중복 소유함을 의미합니다.
위에서 불필요한 중복을 제거하기 위해 상속을 구현한다고 말했었죠.
그런데 생성자 함수로 생성한 모든 인스턴스의 메서드가 모두 중복되네요..
❗️ 이는 인스턴스를 생성할 때마다 동일한 메서드를 생성하므로, 성능 저하 및 불필요한 메모리를 낭비를 초래하게 됩니다.
그럼 위 예시를 프로토타입 상속을 통한 코드로 수정해볼까요?
function Friend(name) {
this.name = name;
}
Friend.prototype.introduce = () => {
return `안녕하세요. ${this.name}입니다!`
}
const friend1 = new Friend('짱구');
const friend2 = new Friend('철수');
console.log(friend1.introduce === friend2.introduce);
// true
true !! 불필요한 중복이 사라졌다!
두 인스턴스 모두 같은 메서드를 소유하고 있는 것을 볼 수 있네요.
Friend 생성자 함수의 prototype 프로퍼티로 프로토타입에 접근하여 공유 메서드 introduce 를 생성했습니다.
그리고 Friend 생성자 함수가 생성한 모든 인스턴스는 부모 객체 역할을 하는 프로토타입 Friend.prototype 으로부터 introduce 메서드를 상속받아요.
잘 이해가 안된다구요? 다음 이미지를 봅시다.
즉, name 과 같이 개별적으로 소유하는 프로퍼티만 개별적으로 소유하고, 내용이 동일한 메서드는 프로토타입 상속을 통해 공유하여 사용합니다!
💡 이처럼, 프로토타입은
- 객체 간 상속을 구현하기 위해 사용되며,
- 부모의 역할을 하는 객체로서 다른 객체에 공유 프로퍼티 / 메서드를 제공해요.
모든 자바스크립트 객체는 하나의 프로토타입을 가지고 있어요.
그리고 모든 객체는 [[Prototype]] 이라는 내부 슬롯을 가지고 있구요.
이 내부 슬롯의 값이 바로! 프로토타입의 참조입니다.
그럼 [[Prototype]] 에는 어떻게 접근할까요? [[Prototype]] 에 직접 접근할 수는 없지만 (내부 슬롯이기 때문에!) 간접적으로 접근할 수 있는 프로퍼티가 있어요!
바로, __proto__ !!
내부 슬롯은 프로퍼티가 아니에요. 따라서 자바스크립트는 원칙적으로 내부 슬롯과 내부 메서드에 직접적으로 접근하거나 호출할 수 있는 방법을 제공하지 않아요. 단, 일부 내부 슬롯과 내부 메서드에는 간접적으로 접근할 수 있는 수단을 제공하기는 한답니다.
객체와 생성자 함수, 프로토타입은 서로 연결되어 있어요.
위 이미지로 볼 수 있듯,
- 객체는 __proto__ 프로퍼티를 통해 프로토타입([[Prototype]] 의 값)에 접근할 수 있고,
- 생성자 함수는 자신의 prototype 프로퍼티를 통해 프로토타입에 접근할 수 있으며
- 프로토타입은 자신의 constructor 프로퍼티를 통해 생성자 함수에 접근할 수 있습니다.
자, 그럼 __proto__ 프로퍼티부터 한번 살펴볼까요?
__proto__ 접근자 프로퍼티
모든 객체는 __proto__ 접근자 프로퍼티를 통해 자신의 프로토타입, 즉 [[Prototype]] 내부 슬롯에 간접으로로 접근할 수 있어요.
이러한 __proto__ 는 접근자 프로퍼티입니다!
접근자 프로퍼티란 자체적으로 값을 갖지 않고, 다른 프로퍼티의 값을 읽거나 저장할 때 사용하는 접근자 함수([[Get]] , [[Set]]프로퍼티 속성에 할당된 함수) getter/setter 함수로 구성된 프로퍼티를 말해요.
접근자 프로퍼티의 본질은 함수인데, 이 함수는 값을 획득(get)하고 설정(set)하는 역할을 담당합니다.
(접근자 프로퍼티 개념에 대해 잘 모르시면 그냥 넘어가도 좋습니다 😆)
따라서, __proto__ 접근자 프로퍼티는 접근자 함수를 통해 프로토타입을 얻거나 할당해요.
- __proto__ 접근자 프로퍼티로, 프로토타입에 접근하면 getter 함수인 [[Get]] 호출
- __proto__ 접근자 프로퍼티로, 새로운 프로토타입을 할당하면 setter 함수인 [[Set]] 호출
const obj = {};
const parent = { x: 1 };
// getter 함수가 호출되어 객체의 프로토타입 얻음
console.log(obj.__proto__); // [Object: null prototype] {}
// setter 함수가 호출되어 객체의 프로토타입 교체
obj.__proto__ = parent;
console.log(obj.__proto__); // { x: 1 }
console.log(obj.x); // 1
프로토타입을 얻고, 교체하는 것을 볼 수 있죠?
prototype 프로퍼티
함수 객체 만이 소유하는 prototype 프로퍼티는 생성자 함수가 생성하는 인스턴스의 프로토타입을 가리킵니다.
function Friend(name) {
this.name = name;
}
const friend1 = new Friend('짱구');
console.log(Friend.prototype === friend1.__proto__);
// true
따라서 인스턴스의 __proto__ 접근자 프로퍼티가 가리키는 객체와 해당 인스턴스를 생성한 생성자 함수의 prototype 프로퍼티가 가리키는 객체는 당연히 같겠죠?
주의사항❗️ 모든 함수가 prototype 프로퍼티를 소유하는 것은 아닙니다!
// 생성자 함수로서 호출할 수 있는 함수 객체
(function () {}).hasOwnProperty('prototype'); // true
// 화살표 함수
const Friend = name => {
this.name = name;
};
console.log(Friend.hasOwnProperty('prototype')); // false
console.log(Friend.prototype); // undefined
// ES6의 메서드 축약 표현으로 정의한 메서드
const obj = {
foo() {}
};
console.log(obj.foo.hasOwnProperty('prototype')); // false
console.log(obj.foo.prototype); // undefined
생성자 함수로서 호출할 수 없는 함수, 즉 non-constructor 인 함수는 prototype 프로퍼티를 소유하지 않으며 프로토타입도 생성하지 않아요!
모든 함수 객체는 호출할 수 있지만 모든 함수 객체를 생성자 함수로서 호출할 수 있는 것은 아닙니다.
생성자 함수로 호출할 수 있는 함수 객체는 내부 메서드 [[Construct]] 를 가집니다. 반대로 [[Construct]] 를 가지고 있지 않은 함수 객체, 즉 일반 함수로서만 호출할 수 있는 객체를 ‘non-constructor’ 라 하는데, 화살표 함수와 ES6의 메서드 축약 표현으로 정의한 메서드가 이에 해당됩니다.
constructor프로퍼티
모든 프로토타입은 constructor 프로퍼티를 가지고, 이 프로퍼티는 prototype 프로퍼티로 자신을 참조하고 있는 생성자 함수를 가리킵니다.
function Friend(name) {
this.name = name;
}
const friend1 = new Friend('짱구');
console.log(friend1.constructor === Friend); // true
생성자 함수로 생성한 friend1 객체는 프로토타입 상속을 받아 constructor 프로퍼티를 사용할 수 있고, 이 프로퍼티와 생성자 함수 Friend 가 일치한 것을 볼 수 있죠?
세가지 프로퍼티에 대해 모두 알아보았으니 이제 프로토타입 체인에 대해 알아볼게요~!
프로토타입 체인
사실, 모든 객체는 프로토타입의 계층 구조인 프로토타입 체인에 묶여있어요.
function Friend(name) {
this.name = name;
}
Friend.prototype.introduce = () => {
return `안녕하세요. ${this.name}입니다!`
}
const friend1 = new Friend('짱구');
console.log(friend1.hasOwnProperty('name'));
위 예시에서 Friend 생성자 함수에 의해 생성된 friend1 객체는 Object.prototype 의 메서드인 hasOwnProperty 를 호출합니다.
hasOwnProperty 메서드는 객체라면 모두 사용할 수 있잖아요?!
이것은 friend1 객체가 Friend.prototype 뿐만 아니라! Object.prototype 도 상속받았다는 것을 의미해요.
위 예시를 이미지로 표현하면 다음과 같아요.
자바스크립트 엔진은 객체의 프로퍼티(메서드 포함)에 접근하려고 할 때,
해당 객체에 접근하려는 프로퍼티가 없다면 __proto__ 접근자 프로퍼티가 가리키는 참조를 따라 부모 역할을 하는 프로토타입의 프로퍼티를 순차적으로 검색합니다.
이를!! 프로토타입 체인이라고 해요!
여기서 프로토타입 체인의 종점! 즉, 프로토타입의 최상위 객체가 바로 Object.prototype 입니다!
그렇기 때문에 Object.prototype 객체의 프로퍼티와 메서드는 모든 객체에 상속돼요.
(Object.prototype 의 프로토타입은 null)
우리가 어떤 객체는 동일한 객체 메서드(ex. hasOwnProperty 메서드)를 사용할 수 있는 것이 바로 이 이유입니다.
이처럼 프로토타입 체인은 상속과 프로퍼티 검색을 위한 메커니즘이라 할 수 있어요.
프로퍼티 검색 방향은 한쪽 방향으로만 흘러가야 하기 때문에, 프로토타입 체인은 단방향 링크드 리스트(Linked List)로 구현되어야 합니다.
만약, 순환 참조하는 프로토타입이 체인이 만들어지게 되면?
프로토타입 체인 종점이 사라지겠죠.. 그럼 무한루프에 빠지게 될거에요 😨
따라서! 아무런 체크 없이 무조건적으로 프로토타입을 교체할 수 없도록 해야 합니다!
이때 우리 __proto__ 접근자 프로퍼티가 대신 확인해주고 잘못된 교체의 경우 에러를 발생시켜줘요.
const parent = {};
const child = {};
child.__proto__ = parent;
parent.__proto__ = child;
// TypeError: Cyclic __proto__ value
에러가 발생했네요 😊
.
.
.
프로토타입 관련 프로퍼티들을 통한 프로토타입 접근과 프로토타입 체인에 대해 알아보았어요.
아직 프로토타입 관련 개념들이 많답니다..😅
다음 포스팅에서 프로토타입의 생성 시점 및 방식과 기타 관련 내용들에 대해 다루도록 할게요.
프로토타입, 아주 박살을 내버립시다!!
※ 참조
📖 모던 자바스크립트 Deep Dive (이웅모 지음)