코어 자바스크립트 #6 프로토타입

신윤철·2022년 2월 9일
0

코어자바스크립트

목록 보기
6/8
post-thumbnail

프로토타입

책의 저자는 프로토타입을 이해하고나서 그간 막막했던 자바스크립트의 개념들이 상당히 쉽게 이해가 됐다고 합니다.

저도 프로토 타입을 배우고 나서 많은 것을 이해할 수 있었습니다.

이전엔 콘솔창에 나오는 이런 메소드들도 잘 이해가 안갔는데.. 이제 마음대로 다룰 수 있다는게 가장 큰 메리트인것 같습니다.

이제 본격적으로 프로토 타입에 대해 배워보겠습니다. (중요)

프로토 타입이란?

영어 prototype의 뜻은 원형입니다.

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

즉 이때 사용하는 원형이 prototype입니다.

프로토 타입 개념 이해

개념을 이해하기 전에 각 단어의 의미를 파악하고 시작하겠습니다.

  • constructor : 생성자 함수(원본 객체)
  • prototype : 원본 객체의 원형
  • instance : (constructor을)상속받는 객체
  • __proto__ : 상속받는 객체의 원형 (constructor의 prototype 참조)

var instance = new Constructor(); 을 그림으로 표현하면

이렇게 표현할 수 있습니다.

그림과 코드를 해석해보겠습니다.

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

항상 코드에서 보던 var instance = new Constructor(); 이외에 prototype과 __proto__ 가 등장했고, 이 둘 사이의 관계가 형성됐는데 이 관계가 이번 프로토타입 단원의 핵심입니다.

추후 배우겠지만 prototype은 객체이고 내부에는 메서드들이 정의되어 있습니다.

때문에 prototype을 참조하는 __proto__에서도 prototype의 메서드들이 정의되어 있어 instance에서도 해당 메서드들을 사용할 수 있는 것 입니다.


var instance = new Constructor();

console.dir(instance);
	실행 결과⏬

ES5.1 명세에서는 __proto__가 아니라 [[Prototype]]이라 표현합니다.
사실 __proto__라는 프로퍼티는 브라우저들이 [[Prototype]]을 구현한 대상에 지나지 않았습니다.

때문에 ES6이전엔 instance.__proto__라는 방식으로 접근할 수 없었는데 ES6이후로는 사용할 수 있게 되었습니다.

책에선 이해를 돕기위해 __proto__를 사용하지만
이는 권장되는 코드가 아니므로 실무에서는 instance.__proto__ 대신 Object.getPrototypeOf(instance) / Object.create() 등을 이용하도록 합시다.


prototype과 __proto__ 동작 과정

이제 한번 prototype에 메서드를 지정하고 __proto__ 프로퍼티로 호출하는 실습을 진행해 보겠습니다.

예를 들어, Fruit이란 생성자 함수의 prototype에 getPrice이라는 메서드를 지정한다면

var Fruit = function (price) {
  this._price = price;
}
Fruit.prototype.getPrice = function() {
  return this._price;
};

이제 Fruit 생성자로 만들어진 instance는 __proto__을 통해 getPrice를 호출할 수 있습니다.

var banana = new Fruit("3000");
banana.__proto__.getPrice(); 		// undefined

앞서 배운대로 constructor(Fruit)의 prototype을 instance(banana)의 __proto__가 참조하므로 둘은 각은 객체를 바라봅니다.

그런데 왜 banana.__proto__.getPrice();는 3000이 아닌 undefined 값이 나올까요?

this._price = price;에서 this는 자신을 가리킵니다. 그런데 3장의 this에서 배웠듯이 this가 가리키는 객체가 의도와 빗나가 다른값이 나왔음을 의심할 수 있습니다.

banana.__proto__.getPrice();의 getPrice() 메서드는 banana가 아닌 banana.__proto__를 가리키고 이 객체 내부에는 _price 프로퍼티가 없어 undefined값이 출력된 것 입니다.

만약 __proto__객체에 _price 프로퍼티가 있다면 banana.__proto__.getPrice();도 잘 실행될 것 입니다.

물론 __proto__를 빼고 banana.getPrice();를 실행하면 '3000'이 잘 실행됩니다.
그런데 getPrice는 prototype에 정의된 메서드인데 banana.getPrice();로 getPrice()를 실행한다는 것은 좀 이상합니다.

이는 __proto__생략 가능한 프로퍼티이기 때문입니다.

banana.__proto__.getPrice();에서 __proto__를 생략하면 this는 banana를 가리키고 this를 생략하지 않으면 banana.__proto__를 가리키는것 입니다.

이러한 생략 가능한 성질 덕분에 생성자 함수의 prototype에 어떤 메서드나 프로퍼티가 있다면 instance에서도 마치 자신의 것처럼 해당 메서드나 프로퍼티에 접근할 수 있게됩니다.

실제 실행 결과를 통해 눈으로 prototype과 __proto__의 관계를 확인해 보겠습니다.

var Constructor1 = function (name) {
  this.name = name;
};
Constructor1.prototype.method1 = function () {};
Constructor1.prototype.property1 = 'Constructor Prototype Property1';

var instance1 = new Constructor1('Instance1');
console.dir(Constructor1);
console.dir(instance1);

위 코드의 실행 결과

생성자 디렉터리에는 함수를 나타내는 f, 여러 프로퍼티(argumements, length, name...), prototype, [[prototype]](==__proto__) 등이 존재합니다.

instance 디렉터리에는 name과 [[prototype]](==__proto__)이 존재하는군요.

앞서 배웠듯이 __proto__는 prototype을 참조했으므로 생성자 prototype의 내용과 같은 내용이 들어있을 것입니다.

예상대로 같은 내용이 들어있군요.

덕분에 instance와 instance.__proto__에서도 prototype 메서드의 method1과 프로퍼티인 property1에 접근할 수 있게됩니다.

실행 결과에서 method1과 name, property1의 색상은 보라색이고 나머지 프로퍼티는 연보라색을 띄고 있습니다.

이는 { enumerable : false }라는 속성을 갖고 있어서인데 의미 그대로
{ enumerable : false } 속성을 갖으면 연보라색, 열거 불가능한 속성
{ enumerable : true } 속성을 갖으면 보라색, 열거 가능한 속성을 갖습니다. (for , in등으로 객체 프로퍼티에 접근 가능함)

prototype은 객체의 속성마다 다른 기본 메서드를 갖고 있습니다.
Array.prototype, function.prototype, Object.prototype 등등...

덕분에 배열에선 배열관련 메서드, 객체에선 객체관련 메서드, 함수에선 함수 관련 메서드를 사용할 수 있는 것이죠.

실제 결과로 살펴보겠습니다.

*__proto__는 prototype을 참조한 것이므로 각 prototype에 정의된 기본 메서드라 봐도 무방합니다.

자주 사용하던 hasOwnProperty, bind, map, forEach등은 각각의 property에 정의되어 사용할 수 있던 것이군요.

덕분에 해당 prototype을 참조한 instance에선 본인의 메서드 처럼 호출할 수 있는 것 입니다.

constructor 프로퍼티

이번엔 생성자 함수의 프로퍼티인 constructor 프로퍼티에 대해 알아보겠습니다.

모든 prototype은 constructor을 가지고 있습니다. (__proto__도 마찬가지)

이 프로퍼티는 단어 그대로 원래의 생성자 함수(자기 자신)을 참조합니다.
--> instance로부터 원형이 무엇인지를 알기 위해서 사용

이 constructor 프로퍼티 덕분에 instance에서 직접 constructor에 접근할 수 있는 수단이 생깁니다.

말로는 어려워서 예제를 통해 알아보겠습니다.

var arr = [10, 20, 30];

console.log(Array.prototype.constructor);		// ƒ Array() { [native code] }
console.log(arr.__proto__.constructor);			// ƒ Array() { [native code] }
console.log(arr.constructor);					// ƒ Array() { [native code] }

var arr2 = new arr.constructor(3, 4);			// instance의 constructor 프로퍼티로 생성자 함수 역할을 수행
console.log(arr2);								// [3, 4]

앞서 prototype과 __proto__에 constructor 프로퍼티가 있고 해당 프로퍼티에는 생성자 함수의 정보가 담겨있다고 하였습니다.

이를 이용하여 intance.constructor로(__proto__ 생략) 생성자 함수의 역할을 수행한 코드입니다.

그런데 constructor 프로퍼티는 읽기 속성이 부여된 boolean, string, number) 이외(function, {}, [], new...)에는 임의로 바꿀 수 있다는 위험이 있습니다.

때문에 constructor 프로퍼티에 의존하여 사용하는 것은 안전하지 않습니다.

constructor에 접근하는 다양한 방법
1. Constructor
2. Constructor.prototype.constructor
3. intance.__proto__.constructor
4. instance.constructor
5. Object.getPrototypeOf(instance).constructor

prototype에 접근할 수 있는(사용하는) 여러 방법
1. Constructor.prototype
2. instance.__proto__
3. instance
4. Object.getPrototypeOf(instance) - 2보단 4추천

프로토타입 체인

메서드 오버라이드

만약 생성자 함수에도 getName이라는 메서드가 있고, 생성자 함수로 만든 instance에도 getName이라는 메서드가 있다면 어떻게 될까요?

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

var myName = new Person('윤철');
myName.getName = function () {
  return '신' + this.name;
};
console.log(myName.getName());			// 신윤철

스코프 체인에서 가까운 변수나 함수를 사용하는 것처럼
프로토타입 체인에서도 원본 메서드가 아닌 호출 객체와 가까운 메서드를 사용하게 됩니다.

  1. 자바스크립트 엔진은 가장 가까운 대상인 자신의 프로퍼티에서 메서드를 검색합니다.
  2. 없다면 그 다음으로 가까운 __proto__의 프로퍼티를 검색하는 방식으로 진행됩니다.

여기서 주의할 점은 가까운 __proto__에서 검색된 메서드를 사용해도 같은 이름의 더 멀리 있는 메서드가 사라지지 않는다는 점입니다.

그렇다면 myName instance에서 Person.getName()에 접근하려면 어떻게해야할까요?

이론적으로는 myName.__proto__.getName()을 사용하면 Person의 메서드에 접근할 수 있습니다.

하지만 앞서 배웠던 것처럼 getName()의 this(myName.__proto__)를 가리키고 해당 객체는 name이 없으므로 undefined가 출력됩니다.

myName.__proto__.getName(); 			// undefined
Person.prototype.name = "새로운";		// myName.__proto__와 Person.prototype이 같다는 의미
myName.__proto__.getName(); 			// 새로운

하지만 위의 방법은 새로운 name 프로퍼티를 만드는 것이고 제가 하고싶은것은 myName의 name을 Person.getName()으로 출력하는 것입니다.

이때는 this 파트에서 배운 것처럼 call, apply를 통해 this를 myName으로 지정해줌으로써 해결 가능합니다.

console.log(myName.__proto__.getName.call(myName));
console.log(myName.__proto__.getName.apply(myName));

프로토타입 체인

메소드 오버라이딩에서 프로토타입 체인의 개념을 약간 볼 수 있었습니다.


myName 객체에도 __proto__([[Prototype]])가 존재하고 그 내부에도 __proto__([[Prototype]])가 존재합니다.

이를 그림으로 표현하면

이런식으로 연결됩니다.

덕분에 myName instance에서는 Person의 Object.prototype과 Object의 Object.prototype을 사용할 수 있는 것이죠. (물론 둘은 같은 Object.prototype이라 의미가 없긴합니다.)

만약 중간에 Array.prototype이 있었다면 Array.prototype의 pop, push 메서드 등을 사용할 수 있었을 것입니다.

이처럼 __proto__ 프로퍼티가 연쇄적으로 이어진 것을 프로토타입 체인이라고 합니다.

한가지 알아야할 점은 예시처럼 최상단 prototype은 항상 Object.prototype이란 점입니다.

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

앞서 말했듯 어떤 생성자 함수이든 prototype은 객체이기 때문에 Object.prototype이 프로토타입 체인의 최상단에 존재합니다.

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

Array.prototype.getArray = function() {} ==> 가능
Object.prototype.getObject = function() {} ==> 불가능

왜냐하면 어느 데이터 타입이건 최상단에 Object.prototype을 갖기 때문에 Object 데이터타입에서만 사용할 메서드도 다른 데이터 타입에서 접근 가능하기 때문입니다.

때문에 객체에서만 작동하길 원하는 메서드는 Object.prototype이 아닌 Object에 static 메서드로 부여할 수밖에 없습니다.

또한 생성자 함수인 Object와 만들어진 객체 리터럴 사이에는 this를 통한 연결이 불가능하기 때문에 다른 방식으로 사용해야합니다.
==> 최상단 Object.prototype과 만들어진 객체 리터럴 사이엔몇 단계의 __proto__가 있기 때문에 this로 연결할 수 없음

이것이 instance.getPrototypeOf()가 아닌 object.getPrototypeOf(intance) 방식으로 사용하는 이유입니다.

다중 프로토타입 체인

지금까지는 1단계, 2단계의 낮은 단계인 프로토타입 체인을 알아봤지만 마음만 먹으면 많은 단계를 가진 체인을 만들 수 있습니다.

방법은 __proto__(==생성자 함수의 prototype)가 가리키는 대상이 연결하고자 하는 상위 생성자 함수의 instance를 바라보게 하면 됩니다.

그림으로 보면 한층 이해가 쉽습니다.

Person.prototype = [];

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

var myName = new Person('윤철');

Person.prototype = []을 통해 프로토타입 체인은 아래처럼 합쳐지게 됩니다.

이제 myName의 프로토타입 체인에 Array.prototype이 포함되므로 Array.prototype의 메서드인 push, pop, filter, find 같은 배열 메서드를 사용할 수 있게됩니다.

정리

  • 어떤 생성자 함수를 new연산자나 배열일 경우 var arr = []같이 호출하면 constructor에서 정의된 내용을 바탕으로 새로운 instance가 생성됩니다.

  • instance에는 __proto__라는 constructor의 prototype을 참조하는 프로퍼티가 자동으로 부여됩니다.

  • instance.__proto__.메서드 형식으로 사용할 수 있는데 이때 __proto__는 생략할 수 있어서 instance.메서드처럼 자신의 것인 마냥 사용할 수 있습니다.

  • 프로토타입은 체인처럼 __proto__들로 구성되어 있고 객체들은 필요한 메서드를 체인을 하나씩 탐색하게 됩니다.
    이런 프로토타입 체이닝을 통해 각 프로토타입 메서드를 사용할 수 있게됩니다.

  • Object.prototype은 최상단 prototype으로 모든 객체에서 접근 가능해서 새로운 메서드가 필요하다면 static하게 선언해야합니다.

profile
기본을 탄탄하게🌳

0개의 댓글