프로토타입 vs 클래스
프로그래밍 언어에는 상속을 사용하는 클래스 기반의 언어와 객체를 프로토타입으로 삼고 이를 복제하는 프로토타입 기반 언어가 있습니다.
자바스크립트는 프로토타입 기반의 언어로 만들어져 있습니다.
프로토타입 기반 언어는 객체를 생성할 때 객체를 다른 객체 기반으로 생성합니다.
이를 프로토타입 체인이라고 부르며, 체인 상의 상위 객체를 상속받아 생성하여 상위 객체의 프로퍼티와 메서드를 사용할 수 있습니다.
클래스 기반의 언어는 정형화된 구조를 가지고 있어 코드의 재사용성과 유지보수성이 높지만, 프로토타입 기반의 언어는 프로토타입 체인을 통해 비정형화된 구조를 갖고 있으나, 객체를 유연하게 생성하고 확장할 수 있어 코드의 표현력이 높습니다.
프로토타입 사용 목적
프로토타입은 자바에서 static을 사용하는 것처럼 공유 변수를 통한 메모리 최적화를 할 수 있습니다.
function KoreaPerson(name, age) {
this.name = name;
this.age = age;
this.nationality = 'Korea';
}
const personA = new Person('A', 10);
const personB = new Person('B', 20);
KoreaPerson 객체에 이름과, 나이, 국적을 저장하고, personA와 personB 인스턴스를 생성하였습니다.
personA와 B 모두 같은 한국인을 저장하기 위한 인스턴스인데 불필요하게 nationality가 각각의 인스턴스에 존재하여 메모리를 소모하고 있습니다.
function KoreaPerson(name, age) {
this.name = name;
this.age = age;
}
KoreaPerson.prototype.nationality = 'Korea';
const personA = new KoreaPerson('A', 10);
const personB = new KoreaPerson('B', 20);
console.log(personA.nationality);
다음과 같이 프로토타입을 이용해서 공유 변수를 지정해주면, 하나의 공유 변수로 필드를 구성할 수 있습니다.
프로토타입 도식
프로토타입의 핵심 개념은 prototype 프로퍼티와 __proto__ 프로퍼티 입니다.
두 프로퍼티는 각각 객체 생성, 인스턴스 생성 시에 자동으로 생성되며, prototype 객체 내부에는 인스턴스가 사용할 메서드를 저장합니다.
이 prototype을 참조하는 것이 __proto__입니다. __proto__ 프로퍼티의 핵심은 생략 가능한 프로퍼티라는 점입니다. 관련해서 예제에서 다루도록 하겠습니다.
프로토타입을 도식으로 그리자면 위 그림과 같습니다.
- 어떤 Constructor (생성자 함수)를 new 연산자와 함께 호출
- Contructor에서 정의된 내용을 바탕으로 새로운 인스턴스 생성
- instance에 __proto__ 라는 프로퍼티가 자동 부여
- __proto__ 프로퍼티는 Constructor의 prototype을 참조
__proto__는 double underscore의 줄임말로 dunder proto라고 불립니다.
ES5 명세에는 __proto__ 가 아니라 [[prototype]] 으로 명칭돼어 있으며, __proto__와 같이 직접 접근하는 것을 허용하지 않고 Reflect.getPrototypeOf(instance), Object.getPrototypeOf(instance)를 통해서만 접근하도록 정의했습니다.
하지만, 대부분의 브라우저들이 __proto__ 를 직접 접근하는 방식을 포기하지 않았고, 결국 ES6부터 정식으로 브라우저에 한해서 정식으로 인정하기에 이르렀습니다. 따라서 브라우저 환경이 아닌 다른 환경에서는 __proto__를 사용을 권장하지 않습니다.
프로토타입 예제
let Person = function(name) {
this._name = name;
};
Person.prototype.getName = function() {
return this._name;
};
위의 코드는 Person이라는 생성자 함수에 getName이라는 메서드를 생성하였습니다.
이제 Person을 new를 통해 생성한 인스턴스는 __proto__를 사용하여 getName 메서드를 호출할 수 있습니다.
const suzi = new Person('Suzi');
console.log(suzi.__proto__.getName()); // undefined
console.log(suzi.getName()); // Suzi
console.log(Person.prototype === suzi.__proto__); // true
suzi라는 Person 인스턴스를 생성하여 suzi.__proto_.getName()을 통해 __proto__ 프로퍼티가 참조하는 getName() 메서드를 호출했지만 undefined가 출력되고, suzi.getName() 은 잘 출력됩니다.
메서드에 대해 다시 되돌아봅시다. 메서드는 객체 안에서 사용되기 위한 함수로 .앞에 까지가 this 스코프가 됩니다.
따라서 suzi.__proto__. getName()을 하게 되면 현재의 this 스코프는 suzi.__proto__가 되겠죠.
suzi.__proto__ 스코프에서 접근할 name 프로퍼티가 존재하지 않기 때문에 undefined가 출력됩니다.
반대로 suzi.getName()은 현재 this 스코프가 suzi 객체인 상황이고, 이 객체 스코프에선 name을 접근할 수 있기 때문에 원하는 값이 출력됩니다.
인스턴스인 suzi는 __proto__를 통해 prototype을 참조하게 되어있는데, 생략 가능한 프로퍼티이기 때문에 __proto__를 생략해야만 접근이 가능한거죠.
suzi.__proto__._name = 'Suzi';
console.log(suzi.__proto__.getName()); // Suzi
다음과 같이 suzi.__proto__ 에서 name 프로퍼티를 지정해 준다면 우리가 원하는 값을 볼 수 있습니다.
Constructor 객체와 Constructor의 인스턴스인 instance를 생성하여 크롬 개발자 도구로 확인해 보면 다음과 같이 출력됩니다.
인스턴스의 __proto__는 객체의 prototype을 참조하기 때문에 같은 정보를 갖고 있네요.
arr이라는 배열을 생성해서 디렉토리 구조를 출력해 보면 위의 그림과 같이 나옵니다.
첫 줄에 Array(2)라고 표기된 것은 Array라는 생성자 함수를 원형으로 삼아 생성돼었다는 의미입니다. (프로토타입 언어 방식)
[[prototype]] (__proto__) 에는 Array 객체에서 사용할 수 있는 메서드들이 모두 들어 있습니다.
즉, Array 객체의 prototype을 참조하여 arr 인스턴스의 [[prototype]] 정보에 담기게 됩니다.
constructor 프로퍼티
프로토타입 객체 내부에는 constructor 프로퍼티가 존재합니다. 인스턴스인 __proto__ 내부에도 마찬가지로 constructor 프로퍼티가 존재합니다.
const arr = [1, 2];
console.log(Array.prototype.constructor === Array); // true
console.log(arr.__proto__.constructor === Array); // true
console.log(arr.constructor === Array); // true
constructor 프로퍼티를 통해 인스턴스로부터 그 원형이 무엇인지를 알 수 있습니다.
const Person = function (name) {
this.name = name;
};
const p1 = new Person('sasca');
const p1Proto = Object.getPrototypeOf(p1);
const p2 = new Person.prototype.constructor('sasca2');
const p3 = new p1Proto.constructor('sasca3');
const p4 = new p1.__proto__.constructor('sasca4');
const p5 = new p1.constructor('sasca5');
[p1, p2, p3, p4, p5].forEach( p => console.log(p instanceof Person)); // true
constructor를 사용해서 다양하게 객체를 생성할 수 있습니다. 위의 방식 모두 Person 인스턴스가 됩니다.
프로토타입 체인
const arr = [1,2];
console.dir(arr);
위의 코드를 브라우저에서 출력하면 [[prototype]] : Array 하위에 [[Prototype]] : Object가 존재합니다.
그 이유는 prototype이 객체 타입이기 때문입니다. 기본적으로 모든 객체의 [[Prototype]]에는 Object.prototype이 연결됩니다.
위의 arr 인스턴스의 프로토타입을 도식화하면 다음과 같습니다.
__proto__은 생략 가능하기 때문에 arr 인스턴스는 Array.prototype과 Object.prototype의 내부 메서드를 자신의 것처럼 실행할 수 있게 됩니다.
Array 객체뿐만 아니라 Number, String, Boolean 등 다양한 데이터 타입들 모두 동일한 형태의 프로토타입 체인 구조를 갖게 됩니다.
프로토타입 체인상 가장 마지막은 Object.prototype이 존재합니다.
다만, 예외적으로 Object.create(null)를 사용한 경우 Object.prototype이 없는 객체를 생성합니다. 이 메서드를 사용하면, 내장 메서드 및 프로퍼티들이 제거됨으로써 제약이 생기지만, 객체의 무게가 가벼워지는 성능상 이점을 가져갈 수 있습니다.