프론트엔드 개발

코어 자바스크립트 - 프로토타입 본문

후기/코어 자바스크립트

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

태나미 2021. 10. 8. 01:58
이 글은 코어 자바스크립트 책에서 프로토타입을 공부하고 정리하는 목적으로 남깁니다.

목표

  • 어떤 생성자 함수를 함께 호출하면 새로운 인스턴스가 생성되는 과정을 알 수 있습니다.
  • 프로토타입과 프로토타입 체이닝을 알 수 있습니다. 

목차

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

프로토타입 개념 이해

자바스크립트는 프로토타입 기반 언어입니다. 위키백과에서 프로토타입 기반 언어를 다음과 같이 설명합니다.

프로토타입 기반 언어는 클래스 기반 언어에서 상속을 사용하는 것과는 다르게, 객체를 원형(프로토타입)으로 하는 복제 과정을 통해 객체의 동작 방식을 재사용 할 수 있게 한다. - 위키백과

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

constructor, prototype, instance

var instance = new Construtor();

위 코드를 도식 형태로 표현하면 다음과 같습니다.

프로토타입 도식

  • 어떤 생성자함수(constructor)를 new 연산자와 함께 호출하면 새로운 instance가 생성됩니다.
  • 이때 instance에는 __proto __라는 프로터피가 자동으로 부여됩니다.
  • 이 프로퍼티는 contructor의 prototype 프로퍼티를 참조합니다.

prototype 객체 내부에는 인스턴스가 사용할 메서드를 저장하고, 인스턴스에서도 __proto__를 통해 이 메서드들에 접근할 수 있게 됩니다.

예시 1)  Person.prototype

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

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

var tae = new Person("tae");
tae.__proto__.getName();             // undefined

Person.prototype === tae.__proto__ ; // true

instance의 __proto__ 가 constructor의 prototype 프로퍼티를 참조하므로 둘은 같은 객체를 바라보게 됩니다. (true 부분) 여기서, tae.__proto__.getName()메서드를  호출하면 undefined가 반환되는 것을 볼 수 있는데,  this에 바인딩된 대상이 잘못 지정되었기 때문임을 알 수 있습니다. ( tae.__proto__ 객체 내부에는 찾는 식별자가 정의되어 있지 않으면 undefined를 반환합니다 )

 

예시 1에 getName 메서드 부분만 수정

...
tae.getName();                // 'tae'

위 코드는  __proto__생략 가능한 프로퍼티임을 나타냄을 알 수 있습니다. 또한 this는 tae instatnce를 바라보게 할 수 있게 된 것입니다. 

 

내장 생성자 함수인 Array를 알아보겠습니다

const arr = [1,2];
console.dir(arr);
console.dir(Array);

arr(왼쪽) 과 Array(오른쪽)의 구조

짙은색: enumerable, 열거 가능한 프로퍼티

옅읕색: unnumerable, 열거 불가능한 프로퍼티

배열 리터럴과 Array의 관계

바로 위의 도식에서, 배열 리터럴이든, 인스턴스를 생성하든 instance인 [1,2]가 만들어집니다. 이 인스턴스의 __proto__(생략 가능한 프로퍼티)는 Array.prototype을 참조하는데, 인스턴스가 push, pop, forEach 등의 메서드를 자신의 것처럼 호출할 수 있는 이유를 알 수 있습니다. 

var arr = [1, 2];
arr.forEach(function() {});
Array.isArray(arr);
arr.isArray();            // TypeError: arr.isArray is not a function

그러나 Array의 prototype프로퍼티 내부에 있지 않는 from, isArray등은 인스턴스가 직접 호출할 수 없어 TypeError가 나게되는데, Array 생성자 함수에서 직접 접근하면 Error가 나지 않습니다.

constructor 프로퍼티

예시 2)

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);
console.log(arr2);                     // [3, 4]

생성자 함수의 프로퍼티인 prototype객체 내부에는 constructor라는 프로퍼티를 볼 수 잇습니다. 이  프로퍼티의 특징은

  • 원래의 생성자 함수를 참조하며 인스턴스로부터 그 prototype이 무엇인지 알 수 있습니다.
  • 읽기 전용 속성이 부여된 예외적인 경우( number, string, boolean )를 제외하고 값을 바꿀 수 있습니다.

예시 3) - 다양한 constructor 접근 방법

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

var p1 = new Person('사람1');
var p1Proto = Object.getPrototypeOf(p1);
var p2 = new Person.prototype.constructor('사람2');
var p3 = new p1Proto.constructor('사람3');
var p4 = new p1.__proto__.constructor('사람4');
var p5 = new p1.constructor('사람5');

[p1,p2,p3,p4,p5].forEach(function(p){
  console.log(p, p instanceof Person);
});

// Person {name: '사람1'} true
// Person {name: '사람2'} true
// Person {name: '사람3'} true
// Person {name: '사람4'} true
// Person {name: '사람5'} true

프로토타입 체인

메서드 오버라이드

만약 인스턴스가 동일한 이름의 프로퍼티 또는 메서드를 가진다면??

예시 4) - 메서드 오버라이드

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

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

var taenami = new Person('taenami');
taenami.getName = function () {
  return 'good ' + this.name;
};

console.log(taenami.getName()); // good taenami

메서드 오버라이드, 메서드 위에 메서드를 덮어씌웠다는 표현입니다. 

자바스크립트 엔진이 getName이라는 메서드를 찾는 방식은 가까운 대상인 자신의 프로퍼티를 검색하고, 없으면 그다음으로 가까운 대상인 __proto__를 검색하는 순서로 진행됩니다. taenami 객체에 getName이라는 메서드가 있어 taenami 객체에 있는 getName의 메서드가 호출되었습니다.

프로토타입 체인

Object.prototype 내부의 메서드도 자신의 것처럼 실행할 수 있습니다. 생략가능한 __proto__를 한번 더 따라가면, Object.prototype을 참조할 수 있기 때문입니다.

프로토타입 체인, 어떤 __proto__ 프로퍼티 내부에서 다시 __proto__ 프로퍼티가 연쇄적으로 이어진 것입니다. 이 체인을 따라 검색하는 것은 프로토타입 체이닝이라고 합니다. 이 프로토타입 체이닝을 통해 각 프로토타입 메서드를 자신의 것처럼 호출할 수 있습니다.

프로토타입 체이닝은 어떤 메서드를 호출하면 자바스크립트 엔진은 자신의 프로퍼티들을 검색해서 원하는 메서드가 있으면 그 메서드를 실행하고, 없으면 __proto__를 검색해서 있으면 그 메서드를 실행하고, 없으면 다시 __proto__를 검색해서 실행하는 식으로 진행합니다.

예시 5)  메서드 오버라이드와 프로토타입 체이닝

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
  • arr.__proto__는 Array.prototype 참조
  • Array.prototype.__proto__ 는 Object.prototype 참조

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

어떤 생성자 함수이든 prototype은 반드시 객체이기 때문에 Object.prototype이 언제나 프로토타입 체인의 최상단에 존재하게 됩니다. 객체에서만 사용할 메서드는 프로토타입 객체 안에 정의할 수가 없습니다.

예외적으로, Object.create를 이용하면 Object.prototype의 메서드에 접근할 수 없는 경우가 있습니다.

var _proto = Object.create(null);
_proto.getValue = function(key){
  return this[key];
};

var obj = Object.create(_proto);
obj.a = 1;

console.log(obj.getValue('a'));    // 1
console.dir(obj);

_proto에는 __proto__ 프로퍼티가 없는 객체를 할당했습니다. 다시 obj는 앞서 만든 _proto를 __proto__로 하는 객체를 할당했습니다.  이때 obj는  __proto__ 에는 getValue 메서드만 존재하고, __proto__ 및 constructor 프로퍼티 보이지 않습니다. 객체 자체의 무게가 가벼워짐으로써 성능상 이점을 가집니다.


정리

__proto__

  • 어떤 생성자 함수를 new 키워드와 함께 호출하면 Constructor에서 정의된 내용을 바탕으로 새로운 인스턴스 생성됨
  • 이 인스턴스에는 __proto__라는, ”Constructor의 prototype 프로퍼티를 참조”하는 프로퍼티가 자동으로 부여됨
  • __proto__는 생략 가능한 속성 이기 때문에 인스턴스는 Constructor.prototype의 메서드를 자신의 메서드인 것처럼 호출할 수 있음

Constructor.prototype

  • constructor라고 다시 생성자 함수 자신을 가리키는 프로퍼티를 가지고 있음

프로토타입 체이닝

  • __proto__ 방향을 계속 찾아가면 최종적으로 Object.prototype에 당도함
  • __proto__ 안에 다시 __proto__를 찾아가는 과정을 프로토타입 체이닝이라고 함
  • 각 프로토타입 메서드를 자신의 것처럼 호출할 수 있음
  • 접근 방식은 자신으로부터 가장 가까운 대상부터 점차 먼 대상으로 나아가고, 원하는 값 찾으면 검색 중단함

Object.prototype

  • 모든 데이터 타입에서 사용할 수 있는 범용적인 메서드만이 존재
  • 객체 전용 메서드는 여느 데이터 타입과 달리 Object 생성자 함수에 스태틱하게 담겨있음

 

출처: https://ui.toast.com/weekly-pick/ko_20160603

 

이미지 출처: https://velog.io/@iamjoo/%EC%BD%94%EC%96%B4-%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-06.-%ED%94%84%EB%A1%9C%ED%86%A0%ED%83%80%EC%9E%85

Comments