모던 자바스크립트 딥다이브 책 260p 19장 프로토타입을 이해하며 정리한 게시물입니다.
자바스크립트는 명령형, 함수형, 프로토타입 기반 객체지향 프로그래밍을 지원하는
멀티 패러다임 프로그래밍 언어이다.
- 모던 자바스크립트 딥다이브 259P
프로토타입이란?
객체에 접근하기 위한 접근자 프로퍼티이다.
모든 객체는 __proto__
접근자 프로퍼티를 통해 자신의 프로토타입 ([[prototype]]
) 내부 슬롯에 간접적으로 접근할 수 있다.
👨🏫 프로토타입 이해하기
- 자바스크립트는 프로토타입 기반의 객체 지향 프로그래밍 언어이다.
- 클래스 (ES6)는 프로토타입 기반 객체 생성 패턴의 새로운 객체 생성 메커니즘이다.
- 프로토타입을 이해하기 전에 객체지향 프로그래밍 먼저 이해해보자.
☑️ 객체지향 프로그래밍
객체지향 프로그래밍은 전통적인 명령령 프로그래밍의 절차지향적 관점에서 벗어나 여러 개의 독립적 단위(=객체의 집합)로 프로그램을 표현하려는 프로그래밍 패러다임을 말한다.
const Object = { name: "Lee" } // key : value 형태로 이루어진 속성을 갖고 있는 Object 변수
객체지향 프로그래밍에서는 객체의 속성을 key: value
형태의 구조를 갖고 있는 단순한 값뿐만아니라, 객체의 상태를 나타내는 데이터와 상태 데이터를 조작할 수 있는 동작을 하나의 논리적인 단위로 묶은 복합적인 자료구조라고 볼 수 있다. 이때 객체의 상태 데이터를 프로퍼티, 동작을 메서드라 부른다.
각 객체는 고유의 기능을 갖는 독립적인 부품으로 볼 수 있지만 자신의 고유한기능을 수행하면서 다른 객체와 관계성을 가질 수 있다.
다른 객체와 메시지를 주고받거나 데이터를 처리할 수 있다. 다른 객체의 상태 데이터나 동작을 상속받아 사용하기도 한다.
상속은 어떤 객체의 프로퍼티 또는 메서드를 다른 객체가 상속받아 그대로 사용할 수 있는 것을 말한다.
자바스크립트는 프로토타입을 기반으로 상속을 구현하여 불필요한 중복을 제거한다.
이 때 코드의 재사용으로 불필요한 중복을 제거할 수 있다.
상속을 통해 불필요한 중복 코드 제거의 예시를 살펴보자
function Circle(radius) {
this.radius = radius;
this.getArea = function () {
return Math.PI * this.radius *2
};
}
const circle1 = new Circle(1);
const circle2 = new Circle(2);
console.log(circle1.getArea === circle2.getArea) // false;
동일한 생성자 함수에 의해 생성된 모든 인스턴스가 동일한 메서드를 중복 소유하는 것은 메모리를 불필요하게 낭비하고 인스턴스를 생성할 때마다 메서드를 생성하므로 퍼포먼스에도 악영향을 준다.
자바스크립트의 프로토타입으로 상속을 구현해보자
function Circle(radius) {
this.radius = radius;
}
Circle.prototype.getArea = function () {
return Math.PI * this.radius *2
};
const circle1 = new Circle(1);
const circle2 = new Circle(2);
console.log(circle1.getArea === circle2.getArea) // true;
위 코드처럼 객체 리터럴에 의해 생성된 객체의 프로토타입은 Object.prototype
이고 생성자 함수에 의해 생성된 객체의 프로토타입은 생성자 함수의 prototype 프로퍼티에 바인딩되어 있는 객체이다. 프로토타입은 자신의 constuctor( 생성자함수 )프로퍼티를 통해 생성자 함수에 접근할 수 있고, 생성자 함수는 자신의 prototype프로퍼티를 통해 프로토타입에 접근할 수 있다.
☑️ 프로토타입 접근자 프로퍼티 __proto__
접근자 프로퍼티는 자체적으로 값 [[value]]
프로퍼티 어트리뷰트를 갖지 않고 다른 데이터 프로퍼티의 값을 읽거나 저장할 때 사용하는 접근자 함수 (accessor function)인 [[get]]
, [[set]]
프로퍼티 어트리뷰트로 구성된 프로퍼티이다.
__proto__
로 접근하면 접근자 프로퍼티의 getter 함수인[[get]]
이 호출된다__proto__
로 새로운 프로토타입을 할당하면 접근자 프로퍼티의 setter 함수인[[set]]
이 호출된다.
✔️ 프로토타입 체인의 최상위 객체는 Object.prototype
__proto__
접근자 프로퍼티는 객체가 직접 소유하는 프로퍼티가 아니라 Object.prototype의 프로퍼티다. 모든 객체는 상속을 통해 Object.prototype.__proto__
접근자 프로퍼티를 사용할 수 있다.
- 프로토타입에 접근하기위해 접근자 프로퍼티를 사용하는 이유는 상호 참조에 의해 프로토타입 체인이 생성된는 것을 방지하기 위해서다.
const parent = {};
const child = {};
child.__proto__ = parent;
parent.__proto__ = child; // TypeError: Cyclic **proto** value
✔️ 프로토타입 접근자 프로퍼티( __proto__
)를 코드 내에서 직접 사용하는 것을 권장하지 않는다.
모든 객체가 __proto__
접근자 프로퍼티를 사용할 수 있는 것은 아니다.
__proto__
접근자 프로퍼티 대신 프로토타입을 참조를 취득하고 싶을 때는 Object.getPrototypeOf메서드를 사용 ( =get Object.prototype.__proto__
)- 프로토타입을 교체하고 싶은 경우 Object.setPrototypeOf 메서드 사용 ( =
set Object.prototype.__proto__
)
✔️ 함수 객체는 Prototype Property를 소유, 일반 객체는 Prototype Property를 소유하지 않는다.
(function(){}).hasOwnProperty('prototype') // true
({}).hasOwnProperty('prototype') // TypeError: (intermediate value).hasOwnProperty(...) is not a function
생성자 함수로서 호출할 수 없는 함수인 화살표 함수와 ES6 메서드 축약 표현으로 정의한 메서드는 prototype 프로퍼티를 소유하지 않으며 프로토타입도 생성하지 않는다.
- 함수 객체만이 가진 Prototype과 일반 객체의
__proto__
접근자 프로퍼티는 동일한 프로토타입을 가진다.
function Person(name) {
this.name = name;
}
const me = new Person("Lee");
console.log(Person.prototype === me.__proto__ ); // true
즉, 생성자 함수 ( constructor )는 Object.prototype 프로퍼티를 갖고 있어 프로토타입을 다루려면 Object.prototype.__proto__
로 접근해야하는데 일반 객체일 경우 모든 객체가 갖고 있는 접근자 프로퍼티인 val.__proto__
(= Object.prototype으로부터 상속받은 __proto__
)로 접근해야한다는 말인 거 같다.
프로토타입의 constructor 프로퍼티와 생성자 함수
위에서 다룬 개념으로 봤을 때, 접근자 프로퍼티인 __proto__
가 Object.prototype의 하위 집합이라 이해하면 될 것 같다. 그렇다면 .prototype
의 프로퍼티에도 상위 집합인 Object.
을 참조할 수 있는 프로퍼티가 있을 것이다. 모든 프로토타입은 자신을 참조하고 있는 생성자 함수를 가리키는 constructor 프로퍼티를 갖는다.
new
키워드로 생성자 함수를 새로운 객체에 할당했을 때 해당 객체에서새로운_객체.constructor
로 생성자 함수에 접근할 수 있다
function Person(name) {
this.name = name;
}
const me = new Person("Lee");
console.log(me.constructor === Person ); // true
- me 객체에는 constructor 프로퍼티가 없지만 me 객체의 프로토타입인 Person.prototype에 constructor 프로퍼티가 있다.
- me 객체는 프로토타입인 Person.prototype의 constructor 프로퍼티를 상속받아 사용할 수 있다.
✔️ 프로토타입과 생성자 함수는 단독으로 존재할 수 없고 언제나 쌍으로 존재한다.
리터럴 표기법에 의해 생성된 객체는 생성자 함수에 의해 생성된 객체는 아니다. 하지만 큰 틀에서 보면 리터럴 표기법으로 생성한 객체도 생성자 함수로 생성한 객체와 본질적인 면에서 큰 차이는 없다. 생성 과정에 미묘한 차이는 있지만 결국 객체로서 동일한 특성을 갖는다.
아래는 리터럴 표기법에 의해 생성된 객체의 생성자 함수와 프로토타입이다.
리터럴 표기법 | 생성자 함수 | 프로토타입 |
---|---|---|
객체 리터럴 | Object | Object.prototype |
함수 리터럴 | Function | Function.prototype |
배열 리터럴 | Array | Array.prototype |
정규 표현식 리터럴 | RegExp | RegExp.prototype |
☑️ 프로토타입 체인
자바스크립트 객체의 프로퍼티에 접근하려고 할 때 해당 객체에 접근하려는 프로퍼티가 없다면 [[Prototype]]
내부 슬록의 참조를 따라 자신의 부모 역할을 하는 프로토타입의 프로퍼티를 순차적으로 검색하는 것을 프로토타입 체인이라고 한다. 프로토타입 체인의 종점( end of prototype chain )은 Object.prototype이다. 모든 객체는 Object.prototype의 상속을 받는다. 프로토타입 체인의 종점인 Object.prototype에서도 프로퍼티를 검색할 수 없다면 에러가 발생하는 것이 아니라 undefined가 출력되는 것에 주의하자. 프로토타입 체인은 프로퍼티 검색을 위한 메커니즘이다.
☑️ 프로토타입 체인과 스코프 체인은 협력하여 식별자와 프로퍼티를 검색하는 데 사용된다.
자바스크립트 엔진은 함수의 중첩 관계로 이루어진 스코프의 계층적 구조에서 식별자를 검색한다. 또한 프로토타입 체인을 따라 프로퍼티/메서드를 검색한다. 객체 간의 상속 관계로 이루어진 프로토타입의 계층적인 구조에서 객체의 프로퍼티를 검색하는 것이다.
☑️ 인스턴스 프로퍼티에 프로토타입 프로퍼티가 소유한 프로퍼티(메서드포함)와 같은 이름인 프로퍼티를 추가하면 어떻게 될까?
✔️ 단어부터 알고가기
- 오버라이딩 : 상위 클래스가 가지고 있는 메서드를 하위 클래스가 재정의하여 사용하는 방식
- 오버로딩 : 함수의 이름은 동일하지만 매개변수의 타입 또는 개수가 다른 메서드를 구현하고 매개변수에 의해 메서드를 구별하여 호출하는 방식. 자바스크립트는 오버로딩을 지원하지 않지만 arguments 객체를 사용하여 구현할 수는 있다.
- 프로퍼티 섀도잉 : 상속 관계에 의해 프로퍼티가 가려지는 현상
const Person = (function() {
// 생성자 함수
function Person(name) {
this.name = name;
}
// 프로토타입 메서드
Person.prototype.sayHello = function () {
console.log(`Hi! My name is ${this.name}`);
};
// 생성자 함수를 반환
return Person;
}());
const me = new Person('Lee');
// 인스턴스 메서드
me.sayHello = function(){
console.log(`Hey! My name is ${this.name}`); // = 오버라이딩
};
// 인스턴스 메서드가 호출된다. 프로토타입 메서드는 인스턴스 메서드에 의해 가려진다 // = 프로퍼티 섀도잉
me.sayHello(); // Hey! My name is Lee
인스턴스 프로퍼티에 프로토타입 프로퍼티가 소유한 프로퍼티(메서드포함)와 같은 이름인 프로퍼티를 추가할 경우 기존 프로토타입이 소유한 프로퍼티를 변경하는 것이 아니라 인스턴스 메서드가 생성되어 me 인스턴스에 의해 sayHello()가 호출될 경우 me 인스턴스 메서드인 sayHello()출력되며 프로토타입 메서드인 sayHello()는 가려지게 된다. 이때 인스턴스 메서드 sayHello는 프로토타입 메서드 sayHello를 오버라이딩했고 프로토타입 메서드 sayHello는 가려지는 것을 프로퍼티 섀도잉이라고 한다.
✔️ 프로퍼티를 삭제하는 경우
delete me.sayHello
로 인스턴스 메서드를 삭제하게 되면 인스턴스에는 sayHello 메서드가 없으므로 프로토타입 메서드가 호출된다.- 하위 객체를 통해 프로토타입의 프로퍼티를 변경 또는 삭제는 불가능하다. 하위 객체를 통해 프로토타입에 get 액세스는 허용되지만 set 액세스는 허용되지 않는다.
- 프로토타입 프로퍼티를 변경 또는 삭제하려면 직접 프로토타입에 접근해야 한다.
delete Person.prototype.sayHello;