constructor, prototype, instance를 이해하기 위해 다음과 같은 그림이 보자.
var instance = new Constructor();
어떤 생성자 함수 Constructor를 new 연산자와 함께 호출하면
Constructor에서 정의된 내용을 바탕으로 새로운 인스턴스 instance가 생성된다.
이때 instance에는 __proto__
라는 프로퍼티가 자동으로 부여되는데 이 프로퍼티는 Constructor의 prototype이라는 프로퍼티를 참조한다.
__proto__
의 관계prototype과 __proto__
모두 객체이다.
prototype은 생성자 함수가 정의될 때 사용되는 프로퍼티이다. 생성자 함수로부터 생성된 모든 객체의 공통된 프로퍼티와 메서드를 포함한다. 같은 생성자로부터 생성된 객체는 프로토타입을 공유하여 메모리를 절약할 수 있다.
__proto__
는 객체의 프로토타입 객체를 가리키는 내부 프로퍼티이다. 이 프로퍼티를 통해 객체는 자신의 프로토타입 객체에 접근할 수 있다. __proto__
를 사용하면 객체는 자신의 프로토타입 객체에 직접 접근할 수 있고, 프로토타입 체인을 따라가면서 상위 프로토타입 객체의 프로퍼티와 메서드를 탐색할 수 있다.
💡 주의사항
__proto__
는 비표준 프로퍼티이므로 권장되는 방법이 아니다. ECMAScript 5부터 이 대신Object.getPrototypeOf
메서드를 사용하여 객체의 프로토타입에 접근하는 것이 권장된다.
prototype과 __proto__
를 이해하기 위한 몇 가지 예를 살펴보자.
var Person = function (name) {
this._name = name;
}
Person.prototype.getName = function() {
return this._name;
}
var sypaik = new Person('Sypaik');
sypaik.__proto__getName()
해당 결과값을 undefined
가 나온다.
에러가 발생하지 않고 undefined가 나온 것은 이 변수가 호출할 수 있는 함수에 해당한다는 것을 의미한다.
➡️ 결론적으로 메서드 내부에서 this에 바인딩된 대상이 잘못 지정되기 때문이다.
Person.prototype.getName 메서드는 this._name 값을 반환하는 역할을 한다. 하지만 sypaik.__proto__getName()
으로 메서드를 호출할 경우, this에 바인딩된 대상은 sypaik.__proto__
즉, Person.prototype
객체가 된다.
Person.prototype 객체에는 _name 프로퍼티가 정의되어 있지 않기 때문에 this._name이 undefined를 반환하게 되어 결과값이 undefined가 되는 것이다.
✅ 올바른 방법은 sypaik 객체 자체에 getName 메서드를 호출해야한다. 즉, sypaik.getName()
으로 호출하는 것이다. 이렇게 하면 this는 sypaik 객체에 바인딩되어 this._name은 Sypaik이 반환된다.
만약 __proto__
객체에 name 프로퍼티가 있다면 어떨까?
var sypaik = new Person('Sypaik');
sypaik.__proto__._name = 'Sypaik__proto__'
sypaik.__proto__.getName(); // Sypaik__proto__
sypaik.getName(); // Sypaik
__proto__
객체에 name 프로퍼티가 있으면 Sypaik__proto__
가 잘 출력되지만 없이도 메서드를 쓸 수 있다.
💡
__proto__
가 생략 가능한 프로퍼티
proto 프로퍼티는 인스턴스와 생성자 함수의 프로토타입 객체 간의 연결을 나타내는 역할을 한다.
앞선 예시에서 자바스크립트 엔진은 인스턴스에서 메서드 또는 프로퍼티를 찾을 때, 먼저 인스턴스 자체에 해당 메서드 또는 프로퍼티가 있는지 확인하고, 없다면__proto__
를 통해 프로토타입 체인을 따라 올라가서 찾게 된다.
따라서__proto__
프로퍼티의 존재로 인해 인스턴스는 생성자 함수의 프로토타입 객체에 정의된 메서드나 프로퍼티를 마치 자신의 것처럼 접근할 수 있다.
var Constructor = function (name) {
this.name = name;
};
Constructor.prototype.method1 = function() {};
Constructor.prototype.property1 = 'Constructor Prototype Property';
var instance = new Constructor('Instance');
console.dir(Constructor);
console.dir(instance);
생성자 함수를 정의하고 해당 생성자 함수의 프로토타입에 메서드와 프로퍼티를 추가한 후 인스턴스를 생성하는 예시이다.
개발자 도구 콘솔에서 해당 코드의 실행한 결과를 확인해보면 다음과 같다.
Constructor의 prototype과 instance의 __proto__
는 동일한 프로토타입 객체를 가리키고 있다.
var arr = [1, 2];
console.dir(arr);
console.dir(Array);
arr 변수와 생성자 함수 Array를 개발자 도구를 통해 확인해보면,
배열 리터럴인 arr 변수의 __proto__
의 메서드와 생성자 함수 Array의 prototype의 메서드가 거의 동일함을 확인할 수 있다.
인스턴스의 proto 는 Array.prototype을 참조하는데 __proto__
가 생략 가능하도록 설계돼 있어 인스턴스가 메서드를 마치 자신의 것처럼 호출할 수 있다는 것이다.
자바스크립트는 객체 지향 프로그래밍 언어로, 객체는 프로퍼티와 메서드를 가질 수 있는 독립적인 개체인데 프로토타입 기반 상속을 지원하기에 객체가 다른 객체로부터 상속받을 수 있다.
객체의 메서드는 해당 객체가 속한 프로토타입 체인에 정의되어 있다. 프로토타입 체인은 해당 객체를 만들 때 사용한 생성자 함수의 프로토타입과 연결된 객체들의 체인이다. 이 체인을 따라 올라가면 해당 메서드를 찾는다. 그러나 기본 데이터 타입들(숫자-Number, 문자열-String)은 객체가 아니며 프로토타입 체인이 없거나 제한적이기 때문에 이러한 메서드를 직접 정의할 수 없다.
Object.prototype.getEntries = function() {
var res = [];
for (var prop in this) {
if (this.hasOwnProperty(prop)) {
res.push([prop, this[prop]]);
}
}
return res;
};
var data = [
['object', {a: 1, b: 2, c: 3}],
['number', 345],
['string', 'abc'],
['boolean', false],
['func', function () {}],
['array', [1, 2, 3]]
];
data.forEach(function (datum) {
console.log(datum[1].getEntries());
});
해당 코드를 실행해보면, 모든 데이터가 오류 없이 결과를 반환하고 있다. 객체가 아닌 다른 데이터 타입에 대해서는 오류가 발생해야하지만 객체인 경우에만 'getEntries' 메서드를 호출하여 객체의 프로퍼티와 값을 배열로 반환하고, 다른 데이터 타입인 경우는 그냥 넘어가는 동작을 수행한다.
객체 전용 메서드는 Object.prototype이 아닌 Object에 스태틱 메서드로 추가되어야 한다.
객체 전용 메서드는 모든 객체가 공통으로 사용할 수 있는 기능을 제공하여 모든 객체의 조상인 Object.prototype에 추가하는 것은 좋지 않다. Object.prototype에 추가된 메서드는 모든 객체에서 호출 가능하기 때문에, 원하지 않는 동작을 초래할 수 있습니다. 따라서 객체 전용 메서드는 Object에 스태틱 메서드로 추가되거나, 다른 방식으로 객체를 인자로 받는 형태로 구현되어야 한다.
또한 생성자 함수인 Object와 인스턴스인 객체 리터럴 사이에는 this를 통한 연결이 불가능하다. this
는 주로 함수 내부에서 사용되며, 해당 함수가 메서드로 호출되는 경우 해당 객체를 가리키게 되는데 객체 리터럴을 사용하여 객체를 생성하는 경우에는 this
가 해당 객체를 가리키지 않기 때문에 객체 리터럴에서는 this
를 사용하여 메서드를 정의할 수 없다. 따라서 this의 사용 대신 대상 인스턴스를 인자로 직접 주입해야하는 방식으로 구현돼 있다.