자바스크립트의 프로토타입

코어자바스크립트 정복 05

Posted by Juri on May 11, 2022

책 코어자바스크립트 (위키북스 출판) 을 참고해 작성한 포스팅입니다.

프로토타입 (prototype)

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

개념 이해

1
var instance = new Constructor();

위의 그림은 아래의 코드를 추상화한 것으로 전체 구조를 파악하는 것을 도와줄 것이다.

윗변의 왼쪽 꼭짓점에는 Constructor(생성자함수)를, 오른쪽 꼭짓점에는 Constructor.prototype이라는 프로퍼티를 위치시켰다. 왼쪽 꼭짓점으로부터 아래를 향한 화살표 중간에 new가 있고 화살표의 끝에 instance가 있다. 오른쪽 꼭짓점으로부터 대각선 아래로 향하는 화살표의 끝에 instance.__proto__라는 프로퍼티가 있다.

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

prototype 객체 내부에는 인스턴스가 사용할 메서드를 저장한다. 그러면 인스턴스에서도 숨겨진 프로퍼티인 __proto__을 통해 이 메서드들에 접근할 수 있게 된다.

예를 들어, Person이라는 생성자 함수의 prototype에 getName이라는 메서드를 지정했다고 치자.

1
2
3
4
5
6
var Person = function (name) {
    this._name = name;
};
Person.prototype.getName = function () {
    return this._name;
};

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

1
2
var juri = new Person("juri");
juri.__proto__.getName(); // undefined

어떤 함수를 메서드로서 호출할 때는 메서드명 바로 앞의 객체가 this가 된다. 따라서, juri.\_\_proto\_\_.getName()에서 getName 함수 내부에서의 this는 juri가 아니라 juri.\_\_proto\_\_가 된다. 이 객체 내부에는 name 프로퍼티가 없으므로 undefined를 반환하는 것이다.

1
2
3
4
var juri = new Person("juri");
juri.getName(); // juri
var jun = new Person("jun");
jun.getName(); // jun

this를 인스턴스로 하기 위해서는 __proto__ 없이 인스턴스에서 곧바로 메서드를 쓰는 방법이 있다. __proto__는 생략 가능한 프로퍼티이기 때문에 가능하다.

자바스크립트는 함수에 자동으로 객체인 prototype 프로퍼티를 생성하는데, new 연산자와 함께 함수를 호출하면 그로부터 생성된 인스턴스에는 숨겨진 프로퍼티인 __proto__가 자동으로 생성되며 이 프로퍼티는 생성자 함수의 prototype 프로퍼티를 참조한다. __proto__ 프로퍼티는 생략이 가능하므로 생성자 함수의 prototype에 어떤 메서드나 프로퍼티가 있다면 인스턴스에서도 마치 자신의 것처럼 해당 메서드나 프로퍼티에 접근할 수 있게 되는 것이다.

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

arr은 Array 생성자 함수의 인스턴스로 이 인스턴스의 __proto__는 Array.prototype을 참조해 인스턴스가 push, pop, foreach 등의 메서드를 호출할 수 있다(1). 한편 Array의 prototype 프로퍼티 내부에 없는 isArray 등의 메서드들은 인스턴스가 직접 호출할 수 없고(3) Array 생성자 함수에서 직접 접근해야 한다(2).

constructor 프로퍼티

생성자 함수의 prototype 객체와 인스턴스의 __proto__ 객체 내부에는 constructor 이라는 프로퍼티가 있다. 이 프로퍼티는 원래의 생성자 함수(자기자신)을 참조한다. 이를 통해 인스턴스로부터 원형이 무엇인지를 알 수 있다.

1
2
3
4
5
6
7
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]

프로토타입 체인

메서드 오버라이드

인스턴스가 동일한 이름의 프로퍼티나 메서드를 갖고 있다면 어떻게 될까?

1
2
3
4
5
6
7
8
9
10
11
12
var Person = function (name) {
    this.name = name;
};
Person.prototype.getName = function () {
    return this.name;
};

var juri = new Person("주리");
juri.getName = function () {
    return "나는 " + this.name;
};
console.log(juri.getName()); // 나는 주리

juri.__proto__.getName이 아닌 juri 객체에 있는 getName 메서드가 호출됐다. 이 현상을 메서드 오버라이드라고 한다.

자바스크립트 엔진이 getName이라는 메서드를 찾기 위해 가장 가까운 대상인 자신의 프로퍼티를 검색하고 이후, 그 다음으로 가까운 대상인 __proto__를 검색하는 순서로 이동한다. 즉, __proto__에 있는 메서드는 자신에게 있는 메서드보다 검색 순서에 밀린 것이다.

메소드 오버라이딩이 되어있는 상태에서 prototype에 있는 메서드에 접근할 수 있을까?

1
console.log(juri.__proto__.getName()); // undefined

this가 juri.__proto__를 가리키는데 prototype상에는 name 프로퍼티가 없어 undefined가 출력된다.

1
2
Person.prototype.name = "장주리";
console.log(juri.__proto__.getName()); // 장주리

원하는 메서드가 호출되고 있는 것을 확인한 후 this가 인스턴스를 바라보도록 바꿔주면 된다.

1
console.log(juri.__proto__.getName.call(juri)); // 주리

프로토타입 체인

객체의 내부 구조를 살펴보자.

Object의 인스턴스임을 알 수 있고, 프로퍼티 a의 값이 보인다. __proto__ 내부에 hasOwnProperty, isPrototypeOf, toLocaleString, toString, valueOf 등의 메서드가 보인다. constructor는 생성자 함수인 Object를 가리키고 있다.

배열의 내부 구조를 살펴보자. (중간 생략)

__proto__안에 다시 __proto__가 있다. 열어보면 객체와 동일한 내용으로 이루어져 있음을 알 수 있다. prototype 객체가 객체이기 때문이다. 기본적으로 모든 객체의 __proto__에는 Object.prototype이 연결된다.

배열의 내부를 그림으로 표현하면 위의 사진과 같다.

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

1
2
3
4
5
6
7
8
9
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은 배열이므로 arr.__proto__는 Array.prototype을 참조하고, Array.prototype은 객체이므로 Array.prototype.__proto__는 Object.prototype을 참조한다. Array.prototype과 Object.prototype 모두 toString이라는 메서드를 갖고 있어 어떤 값이 출력되는 지 확인을 해본 결과 arr.toString과 Array.prototype.toString의 결과값이 동일하다.

arr에 직접 toString 메서드를 부여하니 Array.prototype.toString이 아닌 새로 부여한 메서드가 실행되는 것을 확인할 수 있다.