아카이브/JavaScript

[코어 자바스크립트] 06 프로토타입

kaaay 2021. 12. 7. 16:13
728x90

 

자바스크립트는 프로토타입(prototype) 기반 언어다. 클래스 기반 언어에서는 '상속'을 사용하지만 프로토타입 기반 언어에서는 어떤 객체를 원형(prototype)으로 삼고, 이를 복제(참조)함으로써 상속과 비슷한 효과를 얻는다.

 

  1. 프로토타입의 개념 이해
  2. 프로토타입 체인

1. 프로토타입의 개념 이해

constructor, prototype, instance

var instance = new Constructor()

  • 어떤 생성자 함수(Constructor)를 new 연산자와 함께 호출하면,
  • Constructor에서 정의된 내용을 바탕으로 새로운 인스턴스(instance)가 생성된다.
  • 이때 instance에는 __proto__ 라는 프로퍼티가 자동으로 부여되는데,
  • 이 프로퍼티는 Constructor의 prototype이라는 프로퍼티를 참조한다.

 

✔️ prototype이라는 프로퍼티와__proto__ 라는 프로퍼티의 관계가 프로토타입 개념의 핵심.

 

  • prototype은 객체. 이를 참조하는 __proto__ 역시 객체.
  • prototype 객체 내부에는 인스턴스가 사용할 메서드를 저장.
  • 인스턴스에서도 숨겨진 프로퍼티인 __proto__를 통해 이 메서드들에 접근할 수 있게 된다.

 

❗️ES5.1 명세에는 __proto__가 아니라 [[prototype]] 이라는 명칭으로 정의되어 있으며, instance.__prototype__과 같은 방식으로 직접 접근하는 것을 허용하지 않았다.
실무에서는 가급적 Object.getPrototypeOf()/ Object.create() 등을 이용할 것을 권장한다.

 

[Person.prototype]

var Person = function (name) {
	this._name = name;
};
Person.prototype.getName = function() {
	return this._name; 
};

 

Person의 인스턴스는 __proto__ 프로퍼티를 통해 getName을 호출할 수 있다.

instance의 __proto__가 Constructoer의 prototype 프로퍼티를 참조하므로, 결국 둘은 같은 객체를 바라보기 때문.

 

var suzi = new Person('Suzi');
suzi.__prototype__.getName();  // undefined -> 에러가 발생하지 않았다는 점에 주목!

Person.prototype === suzi.__proto__  // ture

 

어떤 변수를 실행해 undefined가 나왔다는 것은 이 변수가 '호출할 수 있는 함수'에 해당한다는 것을 의미한다.

에러가 발생하지 않았다는 점으로 getName이 실제로 실행됐음을 알 수 있고, 이로부터 getName이 함수라는 것이 입증된다.

 

문제는 this에 바인딩된 대상이 잘못 지정된 것.

getName 함수 내부에서 this는 suzi가 아닌 suzi.__proto__라는 객체.
이 객체 내부에는 name 프로퍼티가 없음.
찾고자 하는 식별자가 정의되어 있지 않으므로 Error대신 undefined를 반환.

만약 __proto__ 객체에 name 프로퍼티가 있다면?

 

var suzi = new Person('Suzi');
suzi.__proto__.name = 'SUZI__proto__';
suzi.__proto__.getName();  // SUZI__proto__

 

this를 인스턴스로 만들면 좋겠다!
👉🏻 __proto__ 없이 인스턴스에서 곧바로 메서드 사용

 

var suzi = new Person('Suzi', 28);
suzi.getName();  // Suzi
var iu = new Person('Jieun', 28);
iu.getName();  // Jieun

 

__proto__가 생략 가능한 프로퍼티이기 때문에 이대로 메서드가 호출되고 원하는 값이 나오는 것.

 

suzi.__proto__.getName
-> suzi(.__proto__).getName
-> suzi.getName

 

__protro__를 생략하지 않으면 this는 suzi.__proto__를 가리키지만, 이를 생략하면 suzi를 가리킨다.

 

 

new 연산자로 Constructor를 호출하면 instance가 만들어지는데 이 instance의 생략 가능한 프로퍼티인 __proto__는 Constructoor의 prototype을 참조한다.

 

__proto__ 프로퍼티는 생략이 가능하도록 구현되어 있다.

때문에, 생성자 함수의 prototype에 어떤 메서드나 프로퍼티가 있다면 인스턴스에서도 마치 자신의 것처럼 해당 메서드나 프로퍼티에 접근할 수 있게 된다.

 

constructor 프로퍼티

생성자 함수의 프로퍼티인 prototype 객체 내부에는 constructor라는 원래의 생성자 함수(자기 자신)을 참조하는 프로퍼티가 있다. 인스턴스와의 관계에 있어 필요한 정보로, 인스턴스로부터 그 원형이 무엇인지를 알 수 있는 수단이다.

 

[constructor 프로퍼티]

var arr = [1,2];
Array.prototype.constructor === Array  // true
arr.__proto__.constructor === Array  // true
arr.constructor === Array  // true

var arr2 = new arr.constructor(3, 4);  // 인스턴스에서 직접 constructor에 접근 가능 
console.log(arr2);  // [3,4]

 

인스턴스의 __proto__가 생성자 함수의 prototype 프로퍼티를 참조하며 __proto__가 생략 가능하기 때문에 인스턴스에서 직접 constructor에 접근할 수 있는 수단이 생긴 것.

한편, constructor는 읽기 전용 속성이 부여된 예외적인 경우(기본형 리터럴 변수-number, string, boolean)를 제외하고는 값을 바꿀 수 있다.

 

[constructor 변경]

var NewConstructor = function () {
	console.log('this is new constructor!');
};
var dataType = [
	1,
  	'test',
  	true,
  	{},
  	[],
  	function () {},
  	/test/,
  	new Number(),
  	new String(),
  	new Boolean,
  	new Object,
  	new Array,
  	new Function(),
  	new RegExp(),
  	new Date(),
  	new Error()
];

dataTypes.forEach(function (d) {
	d.constructor = NewConstructor;
  	console.log(d.constructor.name, '&', d instanceof NewConstructor);
});
}

 

모든 데이터가 d instanceof NewConstructor 명령에 대해 false를 반환한다.

constructor를 변경하더라도 참조하는 대상이 변경될 뿐 이미 만들어진 인스턴스의 원형이 바뀐다거나 데이터 타입이 변하는 것은 아니다.

어떤 인스턴스의 생성자 정보를 알아내기 위해 constructor 프로퍼티에 의존하는 것이 항상 안전하지는 않다.

하지만, 오히려 그렇기 때문에 클래스 상속을 흉내 내는 등이 가능해진 측면도 있다.

 

 


02. 프로토타입 체인

메서드 오버라이드

prototype 객체를 참조하는 __proto__를 생략하면 인스턴스는 prototype에 정의된 프로퍼티나 메서드를 마치 자신의 것 처럼 사용할 수 있다. 그런데 만약 인스턴스가 동일한 이름의 프로퍼티나 메서드를 가지고 있다면?

 

[메서드 오버라이드]

var Person = function(name) {
	this.name = name;
};
Person.prototype.getName = function () {
	return this.name;
};

var iu = new Person('지금');
iu.getName = function () {
	return '바로' + this.name'
};
console.log(iu.getName());  // 바로 지금

 

iu.__prototype__.getName이 아닌 iu 객체에 있는 getName이 호출되었다.

여기서 일어난 현상을 메서드 오버라이드라고 하며, 메서드 위에 메서드를 덮어씌웠다는 뜻이다.

 

자바스크립트 엔진이 getName이라는 메서드를 찾는 방식은 가장 가까운 대상인 자신의 프로퍼티를 검색 -> 없으면 그 다음으로 가까운 대상인 __proto__를 검색 하는 순서로 진행된다.

__proto__에 있는 메서드는 자신에게 있는 메서드보다 검색 순서에서 밀려 호출되지 않은 것.

 

 

메서드 오버라이딩이 이뤄져 있는 상황에서 prototype에 있는 메서드에 어떻게 접근할 수 있을까?

 

console.log(iu.__proto__.getName());  // undefined

Person.prototype.name = '이지금';
console.log(iu.__proto__.getName);  // 이지금

console.log(iu.__proto__.getName.call(iu));  // 지금

 

일반적으로 메서드가 오버라이드 된 경우, 자신으로부터 가장 가까운 메서드에만 접근할 수 있지만 그 다음으로 가까운 __proto__의 메서드로도 우회적인 방법을 통해서 접근 할 수 있다.

 

프로토타입 체인

어떤 데이터의 __proto__프로퍼티 내부에 다시 __proto__ 프로퍼티가 연쇄적으로 이어진 것을 프로토타입 체인이라 하고, 이 체인을 따라가며 검색하는 것을 프로토타입 체이닝이라 한다.

 

[메서드 오버라이드와 프로토타입 체이닝]

var 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

 

객체 전용 메서드의 예외사항

어떤 생성자 함수이든 prototype은 반드시 객체이기 때문에 Object.prototype이 언제나 프로토타입 체인의 최상단에 존재하게 된다.

따라서 객체에서만 사용할 메서드는 다른 여느 데이터 타입처럼 프로토타입 객체 안에 정의할 수가 없다.

객체에서만 사용할 메서드를 Object.prototype 내부에 정의한다면 다른 데이터 타입도 해당 메서드를 사용할 수 있기 때문.

 

이와 같은 이유로, 객체만을 대상으로 동작하는 객체 전용 메서드들은 부득이하게 Object.prototype이 아닌 Object에 스태틱 메서드(static method)로 부여할 수 밖에 없다.

 

다중 프로토타입 체인

두 단계 이상의 체인을 지니는 다중 프로토타입 체인도 가능하며, 이로부터 다른 언어의 클래스와 비슷하게 동작하는 구조를 만들 수 있다.

대각선의 __proto__를 연결하는 방법은 __proto__가 가리키는 대상, 즉 생성자 함수의 prototype이 연결하고자 하는 상위 생성자 함수의 인스턴스를 바라보게끔 해주면 된다.

 

[Grade 생성자 함수와 인스턴스]

var Grade = function () {
 	var args = Array.prototype.slice.call(arguments);
    for (var i = 0'; i < args.length; t++) {
    	this[i] = args[i];
    }
    this.length = args.length;
};
var g = new Grade(100, 80);

 

변수 g는 Grade의 인스턴스를 바라본다.

Grade의 인스턴스는 배열의 형태를 지니지만, 배열의 메서드를 사용할 수 없는 유사배열객체이다.

 

인스턴스에서 배열 메서드를 직접 쓸 수 있게끔 하려면 g.__proto__, 즉 Grade.prototype이 배열의 인스턴스를 바라보게 하면 된다.

Grade.prototype = [];

 

g 인스턴스 입장에서는 프로토타입 체인에 따라 g 객체 자신이 지니는 멤버, Grade의 prototype에 있는 멤버, Array.prototype에 있는 멤버, Object.prototype에 있는 멤버에까지 접근할 수 있게 된다.