Javascript Prototype

앞의 글에서는 Javascript에서 일반적인 객체 개념에 대해 알아보았다. 다시 정리하자면 JS에는 일반적인 class기반 언어에서 이야기하는 class/instance관계가 존재하지 않는다. 모든 것은 object로서 존재한다. JS에서 함수가 new 연산자에 의해 객체 생성에 쓰이는 경우 그 함수를 생성자(constructor)라고 한다. (생성자라는게 따로 존재하는게 아니다.) 따라서 JS의 함수는 객체를 설계하는 class같은 개념이 ’아니다.’

앞의 글에서 new가 함수에 사용되어 객체가 생성되는 경우 다음의 4단계의 동작이 일어난다고 했다.

  • 1) 새로운 객체가 생성된다.
  • 2) 새로 생성된 객체의 [[prototype]] 이 링크된다.
  • 3) 생성자함수의 this가 새로 생성된 객체로 (bind)되어 생성자가 호출된다.
  • 4) 새로 생성된 객체가 리턴된다 (new의 리턴값으로). 단, 한가지 예외가 있는데 해당 생성자가 다른 별도의 객체를 리턴하는 경우다. 이 경우는 새로 생성된 객체는 그냥 버려지고 생성자가 리턴하는 객체가 new의 리턴값이 된다.

이 글에서는 이 중 많은 사람들을 혼동으로 빠트리는 [[prototype]]에 대해서 이야기 해 보겠다.

1. Prototype

JS에서 prototype은 매우 중요하고 핵심적인 내용이다. 그런데 많은 사람들이 이에 대해 힘들어하는 것은 너무나 서로 다른 개념을 모두 ‘prototype’ 이라고 말하는 경우가 많기 때문이다. JS에서 prototype이라고 보통 불리는 것들은 정확히는 다음과 같이 3가지의 서로 다른 개념이 있다. (용어는 완전히 일치하지는 않는다.)

  • Prototype Object (프로토타입 객체)
  • Prototype Link (프로토타입 링크)
  • Prototype Property (프로토타입 프로퍼티)

위 3가지를 명확하게 구분할 수 있어야 JS에서 prototype을 이야기 할 때 혼동을 피할 수 있다.

모든 JS의 객체는 자신이 생성될 때 그 근본이 되는 객체가 있다. 이것이 Prototype Object이다. 일반적으로 그냥 ‘프로토타입’이라고 말할 때는 바로 이 Prototype Object를 의미한다. (보통 ‘어떤 객체의 프로토타입’이라고 말한다.) 모든 객체의 프로토타입은 궁극적으로는 Object.prototype 이라는 객체이다. (그냥 Object가 아니라 Object.prototype임에 유의해야 한다.)

new가 실행될 때의 4단계에서 언급되는 [[prototype]]은 Prototype Link를 의미한다. 일반적인 객체에서는 각 객체의 __proto__ (’_’이 앞 뒤 각각 2개이다.) 프로퍼티로 접근할 수도 있지만 표준이 아니기 때문에 지원안하는 환경이 있을 수 있다. 경우에 따라 [[prototype]]와 proto 를 혼재해 사용하는 경우도 많다.

2. 함수객체의 생성

다음과 같이 MyFunction의 코드가 있다고 가정하자.

var MyFunction = function() {
    this.a = 1;
}

다시말하지만 이건 ‘선언(declaration)이 아니다.’ 위의 코드로도 멀쩡하게 MyFunction이라는 함수 객체가 생성된다.

> MyFunction
[Function: MyFunction]

그럼 함수 객체가 생성되었다고 했으니 a값은 1로 셋팅되었나 확인해 보자

> MyFunction.a
undefined

엇… 그런데 MyFunction이라는 객체에는 a 라는 프로퍼티는 존재하지 않는 것을 알 수 있다.

그럼 도대체 위의 코드로 생성된 것은 무엇인가? 이는 ‘Function.prototype’이라는 객체를 프로토타입으로 해서 생성된 다른 함수 객체이다. 이제부터 이 함수 객체의 몇가지 중요한 개념을 이야기 해 보겠다.

  • JS에서 생성된 모든 객체는 ‘Prototype Link’를 가진다. 보통 [[prototype]]로 표시되는 녀석이다.
  • JS에서 생성된 모든 객체는 그 객체의 생성과 연관되는 ‘Prototype Object’가 존재한다. 이 Prototype Object를 가리키는 것(일종의 포인터로 생각하면 된다.)이 Prototype Link ( [[prototype]] )이다. 이 [[prototype]]도 그 객체의 프로퍼티이지만 뒤의 함수 객체의 Prototype Property와 혼동을 피하기 위해 명확하게 [[prototype]] 또는 Prototype Link라고 불러주는게 좋다.
  • JS의 객체중 ’함수 객체’는 특이한 프로퍼티를 하나 가지는데 그 이름이 ‘prototype’이다. (이게 많은 혼란의 근본이다. 위의 Function.prototype도 이 일종이다. – ‘Function’ 자체도 하나의 함수이다.) 이 Prototype Property는 해당 함수의 ‘Function Prototype Object’를 가리킨다.

위에서 MyFunction로 함수 객체는 생성이 되었지만 해당 함수는 호출(call)되지 않는다. 함수를 호출하려면 명백하게 함수이름 뒤에 ‘()’를 써서 호출을 해야 한다.

위의 설명을 MyFunction 예제를 바탕으로 무슨 일이 일어났는지 다시 설명하면 다음과 같다.

  • 일반적인 다른 언어의 ‘선언부(declaration)’로 ’보이는’ 코드는 실제로 MyFunction 이라고 하는 ’함수 객체’를 생성한다. 함수 객체는 일반적인 객체와 같지만 몇가지 다른 특징을 가지는 객체이다.
  • MyFunction은 다른 일반 객체와 같이 자신의 생성시 바탕이 되는 Prototype Object가 있다. 이는 역시 다른 객체와 같이 MyFunction의 Property Link, 즉 [[prototype]]이 가리키는 객체이다. MyFunction의 [[prototype]]가 가리키는 값은 (즉, MyFunction의 프로토타입은) Function.prototype이라는 객체이다.
  • 그런데 함수 객체는 일반 객체와 달리 이름이 ’prototype’이라는 프로퍼티가 존재한다. (이게 일반적으로 사람들이 혼동을 일으키게 만든다. 이를 명확하게 하기 위해 Prototype Property라고 구분해서 말하기도 한다.
  • 그럼 이 MyFunction의 prototype이라는 프로퍼티, 즉, MyFunction.prototype 이라는 프로토타입 프로퍼티가 가리키는 값은 무었인가. 이 역시 하나의 객체이다. 다만 이는 MyFunction의 프로토타입과는 다른 별도의 객체이다. MyFunction.prototype은 해당 함수가 (그래서 함수 객체에만 존재한다.) 생성자로 쓰일 때 (new로 호출될 때) 생성 될 객체의 기반이 될, 즉 그 생성되는 객체의 프로토타입이 될 객체이다.

3. new를 통한 객체 생성의 예

다시 다음의 코드가 실행된다고 할 때, (다시 말하지만 해당 코드는 실행되지만 MyFunction은 호출이 되지 않는다.)

var MyFunction = function() {
    this.a = 1;
}

메모리에는 다음과 같은 상황이 된다.

함수 객체의 Prototype Property(가 가리키는 객체)에는 특별하게 constructor라는 프로퍼티가 있는데 이는 자신을 활용할 생성자 함수를 다시 가리키고 있다.

이제 다음과 같이 MyFunction으로 new를 이용해 새로운 객체를 생성해 보자.

var MyFunction = function() {
    this.a = 1;
}
var mine = new MyFunction();

>mine.a 
1

이렇게 new를 사용할 때 다음과 같은 과정이 벌어진다.

  • 새로운 객체가 하나 생성된다.
  • 새로운 객체의 프로토타입이 ( = [[prototype]] 의 값이) MyFunction.prototype으로 설정된다. (MyFunction이 아니라 MyFunction.prototype임을 주의해야 한다.)
  • 이 객체의 생성자(MyFunction)의 this가 지금 새로 생성된 객체로 설정되어 실행된다. 이 때 새로운 객체에는 a라는 프로퍼티가 생성된다.
  • 생성자가 다른 객체를 리턴하지 않기 때문에 이 새로 생성된 객체가 new에서 리턴된다. 이 값이 mine에 저장되어 mine은 새로 생성된 객체를 가리킨다.

이후 메모리에는 다음과 같은 상황이 된다.

mine은 MyFunction을 생성자로 해서 (new를 통해) 만들어진 객체이지만 mine의 프로토타입은 MyFunction이 아니라 MyFunction.prototype이라는 함수의 프로토타입 프로퍼티이다.

4. Prototype Chaining과 Shadowing

그러면 이 프로토타입은 어디에 쓰는 것인가? 어떤 객체의 프로퍼티(또는 메소드(프로퍼티가 함수인 경우))가 해당 객체에 없을 경우 JS는 해당 객체의 프로토타입 링크를 따라 ( [[prototype]] 값이 가리키는 값들 ) 거슬러 올라가며 해당 프로퍼티를 찾는다. 이를 프로토타입 체이닝(Prototype Chaining)이라고 한다. 다음의 예제를 보자.

var MyFunction = function() {
    this.a = 1;
}
var mine = new MyFunction();
MyFunction.b = 2;
MyFunction.prototype.b = 3;

>mine.b
3

mine에는 b라는 프로퍼티가 정의되어 있지 않다. 그래서 JS는 프로퍼티링크를 거슬러 가면서 b를 찾게 된다. 여기에서는 b의 값이 3인 것으로 보아 mine의 프로토타입은 MyFunction이 아니라 MyFunction.prototype임을 다시 확인할 수 있다.

어떤 프로퍼티의 값을 읽는 경우 ([[Get]] 오퍼레이션의 경우)에는 사실 큰 문제가 없을 수도 있다. 하지만 새로운 프로퍼티를 객체에 동적으로 할당하는 경우 ([[Put]] 오퍼레이션)에는 좀 다른 문제가 발생한다. 다음과 같은 예를 다시 보자.

var MyFunction = function() {
    this.a = 1;
}
var mine = new MyFunction();
MyFunction.prototype.b = 3;

mine.b = 4
>mine.b
4
>MyFunction.prototype.b
3  

앞에서 mine.b는 프로퍼티 링크를 타고 올라가 MyFunction.prototype.b 의 값을 읽었다. 하지만 mine에 직접 b라는 프로퍼티를 설정하면 b는 직접 mine객체의 프로퍼티로 생성된다. (MyFunction.prototype.b 와는 별개의 프로퍼티가 된다.) 이를 Shadowing이라고 한다.

Shadowing은 어떤 객체에 새로운 프로퍼티를 할당할 때 프로퍼티 링크 상의 상위 객체에 같은 이름의 프로퍼티가 있더라도 그 프로퍼티가 읽기 전용이 아니라면 해당 프로퍼티를 그냥 해당 하위 객체에 바로 생성한다. (상위 객체의 값을 바꾸는 것이 아니라)

상위의 프로퍼티가 읽기 전용이라면 (프로퍼티의 속성을 바꾸는 방법등은 여기서 다루지 않겠다.) 프로퍼티를 할당하려는 시도는 그냥 조용히 실패한다. (strict mode로 동작시에는 에러가 발생한다.) 한가지 특이한 경우는 상위 프로퍼티에 setter가 정의된 경우인데 (다른 언어들과 유사하게 JS의 프로퍼티들도 별도의 getter나 setter를 정의할 수 있다.) 이 경우에는 그 상위 setter가 실행되고 정작 하위의 객체에는 해당 프로퍼티가 생성되지 않는다.

5. Object.create()

생성자 함수를 이용해 new를 통해 객체를 생성하는 것은 위와 같이 매우 복잡한 과정이다. 만일 단순하게 객체를 생성하는 것만이 목적이라면 Object.create() 함수를 이용하면 된다.

var objSource = {
    a: 1,
    b: 2
};

var objNew = Object.create(objSource);

>objNew.a 
1
>objNew.b
2

Object.create 함수는 인자로 받은 객체를 프로토타입으로 하는 새로운 객체를 반환한다. 위의 예제에서 objNew는 Prototype Property가 없이 [[prototype]]이 objSource로 설정되는 객체이다.

6. Inspection

instanceof

JS에서 객체를 다루다보면 각 객체의 연관관계를 검사해야 하는 경우가 많이 생긴다. instanceof는 이 때 사용할 수 있는 연산자(메소드가 아님)이다. 다음의 예를 보자

mine instanceof MyFunction;     // true

instanceof 연산자는 좌항의 객체가 우항의 함수로 생성된 객체인지를 좌항 객체의 [[prototype]]을 거슬러가며 검사한다. 중요한 것은 우항이 항상 ‘함수’여야 한다는 것이다. 이것은 다시 말해 거슬러가면서 검사하는 것은 그 프로토타입 링크의 값이 우항 함수의 프로토타입 프로퍼티(위의 예에서는 MyFunction.prototype)인지를 확인한다는 것이다.

isPrototypeOf()

isPrototypeOf는 연산자가 아니라 Object의 메소드이다. 다시말해 해당 객체가 인자로 받는 객체로부터 생성된 (프로퍼티링크의 체인상에 존재하는) 객체인지를 검사한다. instanceof와 달리 검사하는 대상은 함수가 아니라 객체이다.

objSource.isPrototypeOf(objNew);    // true

getPrototypeOf()

Object의 메소드로 인자로 받은 객체의 프로토타입을 구하는 ( [[prototype]] 값을 알아내는) 공식적인 방법이다. 앞에서 이야기 했듯이 proto 프로퍼티를 쓰는 경우도 있지만 표준이 아니다. (Node.js에서는 지원한다.)

Object.getPrototypeOf(objNew) === objSource;    // true
Object.getPrototypeOf(mine) === MyFunction.prototype;    // true
Object.getPrototypeOf(mine) === MyFunction;    // false

여기까지가 JS의 프로토타입에 대한 중요한 내용들이다. 어떤 함수 객체의 프토토타입 프로퍼티 객체 (AFunction.prototype)는 이 함수를 생성자로 사용해 new로 만든 모든 객체에 프로토타입 링크의 체인으로 공유되기 때문에, 흔히 이 프로토타입 프로퍼티 객체에 프로퍼티나 메소드를 정의해 일종의 객체 상속과 같은 개념으로 활용한다. 하지만 엄밀히 말해 이는 상속이 아니라 일종의 위임(delegation)으로 봐야 한다. 이의 동작의 의미를 명확히 이해하기 위해서라도 프로토타입에 대한 정확한 이해가 중요하다.

다음 글에서는 역시 많은 사람들을 괴롭히는 ‘this’에 대해 이야기 해 보겠다.