Javascript

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

이 챕터를 읽고 난 후 다음의 질문에 답할 수 있어야 한다.

  • 프로토타입이란 무엇인가
  • 프로토타입을 참조하는 인스턴스의 속성은
  • 메서드 오바라이드란
  • 프로토타입 체인이란

 

프로토타입의 개념

자바스크립트는 프로토타입 기반의 언어다.

자바스크립트의 모든 객체들은 어떤 객체를 원형으로 삼고 이를 복제(참조)한다.

객체에 복제되는 속성과 메서드는 자신이 원형으로 삼고 있는 객체의 prototype 속성에 정의되어 있다.

const instance = new Constructor();

 

어떤 생성자 함수를 new 연산자와 함께 호출하면 Constructor에 정의된 내용을 바탕으로 인스턴스가 생성된다. 이때 인스턴스에는 __proto__라는 속성이 부여된다.

 

__proto__는 Constructor의 prototype을 참조한다.

const Constructor = function (name) {
  this.name = name;
};

Constructor.prototype.method1 = function(){};
Constructor.prototype.property1 = 'this is...'

const instance = new Constructor('Instance');

console.log(
    Constructor.prototype === instance.__proto__); // true

console.dir(Constructor);
console.dir(instance);

출력 결과:

인스턴스에 생성자 함수의 속성과 메서드가 복제된 것을 확인할 수 있다.

자바스크립트에는 기본적으로 탑재된 여러 내장 함수가 있다.

대표적으로 Array를 살펴보자.

 

예시. Array

const arr = [1, 2];

console.dir(Array);
console.dir(arr);

출력 결과:

arr에 Array의 프로토타입에 정의된 배열의 기본 메서드가 잘 복제된 것을 확인할 수 있다. 배열을 할당하기만 해도 인스턴스가 만들어진 것이다. 그리고 더 살펴보면, from이나 isArray는 프로토타입 밖에 정의되어 있기 때문에 인스턴스가 참조할 수 없음을 확인할 수 있다. 이렇게 프로퍼티에 정의되지 않아 인스턴스가 참조할 수 없는 메서드나 속성을 각각 스태틱 메서드, 스태틱 프로퍼티라고 부른다.

Array.isArray(arr); // true
arr.isArray() // 타입 에러 발생

인스턴스는 생성자의 스태틱 메서드를 이용할 수 없다.

 

constructor

prototype에는 constructor라는 속성이 있다.

컨스트럭터 속성을 통해 인스턴스와의 관계에서 그 원형이 무엇인지 파악할 수 있다.

참고로 생성자는 스스로가 원형이므로 자기 자신을 참조한다.

console.log(Array.prototype.constructor === Array); // true
console.log(arr.__proto__.constructor === Array); // true
console.log(arr.constructor === Array); // true

인스턴스의 constructor도 생성자를 가리키는 것을 확인할 수 있다.

 

아래와 같이 다양한 방식으로 cunstructor에 접근할 수 있다.

참고로 어떤 생성자의 cunstructor가 바뀌게 되면

그 생성자를 참조하는 인스턴스가 가리키는 cunstructor도 바뀌게 된다.

const Factory = function (name) {
  this.name = name;
};

const f1 = new Factory('공장1');
const f1Proto = Object.getPrototypeOf(f1);
const f2 = new Factory.prototype.constructor('공장2');
const f3 = new f1Proto.constructor('공장3');
const f4 = new f1.__proto__.constructor('공장4');
const f5 = new f1.constructor('공장5');

[f1, f2, f3, f4, f5].
forEach(f => console.log(f, f instanceof Factory));

// Factory {name: '공장1'} true
// Factory {name: '공장2'} true
// Factory {name: '공장3'} true
// Factory {name: '공장4'} true
// Factory {name: '공장5'} true

 

메서드 오버라이드

인스턴스가 동일한 이름의 프로퍼티나 메서드를 가지고 있다면,

오버라이드 현상이 발생한다.

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

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

const iu = new Person('지금');

iu.getName = function(){
  return '바로 ' + this.name;
};

console.log(iu.getName()); // 바로 지금

원본을 제거한 후 교체한 것은 아니다. 원본이 있는 상태에서 덮어 씌었다고 보는 것이 맞다. 이를 오버라이드라고 한다. 자바스크립트 엔진이 가장 가까운 대상을 찾기 때문에 발생하는 현상이다. 여전히 원본은 살아있기 때문에 원본을 호출할 수 있다.

 

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

undefined이 뜨는 이유는 프로토타입의 name 속성 값이 정의되지 않았기 때문이다.

 

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

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

값을 할당하면 원본의 메서드가 제대로 실행된다. call 메서드를 통해 this를 인스턴스에 바인딩할 수 있다.

 

프로토타입 체인

어떤 데이터의 __proto__ 내부에는 또 다른 __proto__가 연쇄적으로 이어져 있다. 이를 프로토타입 체인이라 한다. 어떤 생성자 함수라도 프로토타입 체인을 따라가다 보면 최상단에 객체가 존재한다. 이러한 사유로 객체에는 객체 한정 메서드가 존재한다. 만약 프로퍼티 내부에 객체에서만 사용할 메서드를 정의하게 된다면, 다른 데이터 타입도 이를 상속받아서 사용할 수 있기 때문이다. 참고로 Object.create를 이용하면 __proto__가 없는 객체를 생성할 수 있다.

const _proto = Object.create(null);

_proto.getValue = function(key){
  return this[key];
};

const obj = Object.create(_proto);

obj.a = 1;

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

console.dir(obj);

출력 결과:

기본적으로 내장되어 있는 데이터가 제거됨으로써 기능에 제약이 있지만,

객체가 가벼워져 성능 상에 이점이 있다.

 

다중 프로토타입 체인

__proto__를 이어나가기만 하면 프로토타입 체인을 무한대로 연결할 수 있다.

이를 통해 자바스크립트에서도 다른 언어의 클래스와 유사하게 동작하는 구조를 구현할 수 있다.

const 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;
};

const g1 = new Grade(100, 80);

console.log(g1); // Grade {0: 100, 1: 80, length: 2}

g1은 Grade 생성자 함수에 의해 만들어진 인스턴스이다. 유사 배열 객체인 g1으로 배열 메서드를 적용하고 싶다면 call이나 apply를 활용하는 방법도 있겠지만, 프로토타입을 조작하여 직접 활용하게 할 수도 있다.

Grade.prototype = [];

const g2 = new Grade(100, 80);

console.log(g2) // Grade(2) [100, 80]

g2.pop();

console.log(g2) // Grade [100]

Grade의 프로토타입은 배열의 인스턴스를 참조하고,

Grade의 프로토타입을 참조하는 인스턴스 g2는 배열 메서드를 상속받아 활용할 수 있다.