프로토타입을 한마디로 정의하면 뭐라고 할 것 인가? 그건 바로 유전자!!!!
function Person() {
this.eyes = 2;
this.nose = 1;
}
var kim = new Person();
var park = new Person();
console.log(kim.eyes); // => 2
console.log(kim.nose); // => 1
console.log(park.eyes); // => 2
console.log(park.nose); // => 1
이렇게 하면 김과 박은 둘 다 눈과 코를 가지고 있는데, 메모리에는 눈과 코가 두 개씩 총 4개가 할당된다. 객체를 100개 만들면 200개의 변수가 메모리에 할당되는 magic... 이를 해결하기 위해 프로토타입 개념이 도입된다.
function Person() {}
Person.prototype.eyes = 2;
Person.prototype.nose = 1;
var kim = new Person();
var park = new Person():
console.log(kim.eyes); // => 2
...
Person.prototype이라는 빈 Object(부모유전자)가 어딘가에 존재하고, Person 함수로부터 생성된 객체(김, 박)들은 부모 유전자(객체)에 들어있는 값을 가져다 쓸 수 있다.
그렇다면 본격적으로 프로토타입에 대해 파헤쳐보자!
자바스크립트는 프로토타입 기반의 언어이다.
클래스 기반 언어에서는 상속을 사용하지만 프로토타입 기반 언어에서는 어떤 객체를 원형(prototype)으로 삼고, 이를 복제(참조)함으로써 상속과 비슷한 효과를 얻는다.
<코드 6-1>
var instance = new Contructor();
<그림 6-1>
property에서는 인스턴스가 사용할 메서드를 저장하기 때문에 인스턴스에서도 prototype을 참조하는 __proto__를 통해 메서드들에 접근할 수 있게된다!
<예제 6-1> Person.prototype
var Person = function (name){
this._name = name;
};
Person.prototype.getName = function(){
return this._name;
};
이제 Person의 인스턴스는 __proto__프로퍼티를 통해 getName을 호출할 수 있다.
var somi = new Person('somi');
somi.__proto__.getName(); //undefined
여기서 type error가 아닌 undefined가 나왔다는 것은 이 변수가 호출할 수 있는 함수라는 뜻으로, getName이 실제로 실행되었음을 알 수 있고, 이로부터 getName이 함수라는 것이 입증되었다.
즉, 실행가능한 호출할 수 있는 함수이나, this에 바인딩 된 대상이 잘못 지정되어 있다는 점 때문에 undefined가 리턴된 것이다. somi.__proto__.getName()
에서 getName 함수 내부에서의 this는 somi가 아니라 somi.__proto__
라는 객체가 되는 것이고, 이것에는 name이라는 프로퍼티가 없으므로 undefined가 리턴되었다.
이를 해결하기 위해서는 생략이 가능한 __proto__
의 특성에 따라 이를 생략하고 somi.getName()
이라고 써주면 된다.
결론: 생성자 함수의 prototype에 어떤 메서드나 프로퍼티가 있다면 인스턴스에서도 마치 자신의 것처럼 해당 메서드나 프로퍼티에 접근할 수 있다!
한편 프로퍼티 외부에 있는 메서드들은 인스턴스가 직접 호풀할 수 없기에 생성자 함수에서 직접 접근해야함을 유의하자.
생성자 함수의 프로퍼티인 prototype 객체 내부에는 constructor라는 프로퍼티가 있다. 이는 인스턴스로부터 그 원형이 무엇인지 알 수 있는 수단으로 원래의 생성자 함수를 참고한다.
인스턴스의 __proto__
가 생성자 함수의 prototype 프로퍼티를 참조하며 __proto__
가 생략가능하기에 인스턴스에서 직접 constructor에 접근할 수 있는 수단이 생긴 것!
그래서 아래 <예제 6-2>의 여섯번째 줄과 같은 코드도 오류없이 작동한다.
<예제 6-2>
let arr = [1, 2];
Array.prototype.constructor == Array // true
arr.__proto__.constructor == Array // true
arr.constructor == Array // true
let arr2 = new arr.constructor(3, 4);
console.log(arr2); // [3, 4]
<예제 6-3>
let NewConstructor = function() {
console.log('this is new constructor!');
};
let dataTypes = [
1, // Number & false
'test', // String & false
true, // Boolean & false
{}, // NewConstructor & false
[], // NewConstructor & false
function () {}, // NewConstructor & false
/test/, // NewConstructor & false
new Number(), // NewConstructor & false
new String(), // NewConstructor & false
new Boolean, // NewConstructor & false
new Object(), // NewConstructor & false
new Array(), // NewConstructor & false
new Function(), // NewConstructor & false
new RegExp(), // NewConstructor & false
new Date(), // NewConstructor & false
new Error() // NewConstructor & false
];
dataTypes.forEach(function(d) {
d.constructor = NewConstructor;
console.log(d.constructor.name, '&', d instanceof NewConstructor);
});
모든 데이터가 d instanceof NewConstructor 명령어에 대해 false를 반환하는데, constructor를 변경하더라도 참조하는 대상이 변경될 뿐 이미 만들어진 인스턴스의 원형이 바뀐다거나 데이터 타입이 변하는 것은 아님을 알 수 있다. 어떤 인스턴스의 생성자 정보를 알아내기 위해 constructor 프로퍼티에 의존하는 것이 항상 안전하지는 않다는 것을 알 수 있다.
<예제 6-4>
let Person = function(name) {
this.name = name;
};
Person.prototype.getName = function() {
return this.name;
};
let iu = new Person('지금');
iu.getName = function() {
return '바로 ' + this.name;
};
console.log(iu.getName()); // 바로 지금
인스턴스가 동일한 이름의 프로퍼티 또는 메서드를 가지고 있는 상황이라면, 위의 예제<6-4>와 같이 iu.__proto__.getName
이 아닌 iu객체의 getName이 호출된다. 이렇게 메서드 위에 메서드를 덮어 씌우는 것을 메서드 오버라이드라고 부른다.
만일 인스턴스를 바라보도록 바꿔주고 싶다면 call 이나 apply를 사용하면 된다.
console.log(iu.__proto__.getName.call(iu)); // 지금
prototype 객체는 객체이며, 기본적으로 모든 객체의 __proto__에는 Object.prototype이 연결된다. 이는 prototype 객체도 예외가 아니라서 이를 그림으로 표현하면 아래와 같다.
<예제 6-5>
let arr = [1, 2];
Array.prototype.toString.call(arr); // 1, 2
Object.prototype.toString.call(arr); // [object Array]
arr.toString(); // 1, 2
arr.toString = function() {
return this.join('_');
};
arr.toString(); // 1_2
즉 프로토타입 체인은 어떤 데이터의 __proto__ 프로퍼티 내부에 다시 __proto__ 프로퍼티가 연쇄적으로 이어진 것을 뜻한다.
어떤 생성자 함수이든, prototype은 객체이기때문에 Object.prototype이 언제나 프로토타입 체인의 최상단에 존재하게 된다. 따라서 객체에서만 사용할 메서드는 다른 데이터 타입처럼 프로토타입 객체안에 정의할 수 없다. 객체에서만 사용할 메서드를 이곳에 저장하면 다른 데이터타입도 해당 메서드를 사용할 수 있기 때문!
이러한 이유로 객체만을 대상으로 동작하는 메서드들은 Object.prototype이 아닌 Objcet에 스태틱 메서드(static method)로 부여할 수 밖에 없다. 또한 생성자 함수인 Object와 인스턴스인 객체 리터럴 사이에는 this를 통한 연결이 불가능하기 때문에 여느 전용 메서드처럼 "메서드 명 앞의 대상이 곧 this"가 되는 방식 대신 this의 사용을 포기하고 대상 인스턴스를 인자로 직접 주입해야하는 방식으로 구현돼있다.
같은 이유에서 Object.prototype에는 어떤 데이터에서도 활용할 수 있는 범용적인 메서드들만 있다. toString, hasOwnProperty, valueOf, isPrototypeOf 등은 변수가 마치 자신의 메서드인 것처럼 호출할 수 있다.
대각선의 __proto__
를 연결해나가면 무한대로 체인 관계를 이어나갈 수 있다. 대각선으로 __proto__
를 연결하는 방법은 생성자함수의 prototype이 연결하고자 하는 상위 생성자 함수의 인스턴스를 바라보게끔 해주면 된다.
let Grade = function() {
let args = Array.prototype.slice.call(arguments);
for(let i = 0; i < args.length; i++) {
this[i] = args[i];
}
this.length = args.length;
};
let g = new Grade(100, 80);
Grade의 인스턴스는 여러 개의 인자를 받아 각 순서대로 인덱싱해서 저장하고 length 프로퍼티가 존재하는 등으로 배열의 형태를 지니지만, 배열의 메서드를 사용할 수 없는 유사배열객체이다. 이 인스턴스에서 배열 메서드를 직접 쓸 수 있게 하기 위해서는 g.__proto__
, 즉 Grade.prototype이 배열의 인스턴스를 바라보게 하면 된다.
Grade.prototype = [];