🤔Prototype,,, 잘 아세요?

자바스크립트를 사용하면 누구가 프로토타입을 알 것이고, 공부했을 것이다.
하지만 "프로토타입이 뭔지 설명해줘! " 라는 친구의 물음에 선뜻 설명해줄 수 있는 사람은 많지 않을 것이다..
클래스 비스무리한거 같기도 하고,, 뭐 상속? 받는다고 하던데,,, 프로토타입 체이닝도 중요하다고 하고...
그런데 이제 class문법 지원하는데 알아야 하나? 라는 생각이 들것 같기도 하다...(는 내가 그랬음)

You Don't Konw JS라는 책을 최근에 읽고 있는데 프로토타입에 관한 내용이 정말 그 어떤 블로그 글보다 자세하게 (하지만 길게) 설명되어 있어 책을 기반으로 나름대로 학습한 것들을 덧붙여 적어보았다.


[[Prototype]] 링크

자바스크립트 객체는 Prototype이라는 내부 프로퍼티가 존재한다. 거의 모든 객체가 생성 시점에 이 프로퍼티에 null이 아닌 값이 할당된다.

우선 너무나도 자연스러운 이 코드를 살펴보자.

    const woody = {
        riding:true
    }

    woody.riding // true

woody.riding 처럼 객체 프로퍼티를 참조할 경우 [[Get]]이 호출되어 객체 내부에 해당 프로퍼티가 존재하는지 탐색한다. 그렇다면 객체 내부에 없는 프로퍼티를 호출하게 되면 어떻게 될까??

[[Get]]은 객체 내부에서 해당 프로퍼티를 찾지 못하면 바로 [[Prototype]]링크를 따라가 프로퍼티를 탐색한다. 모든 일반 객체의 최상위 프로토타입 연쇄는 내장 Object.prototype이고 이 지점에서도 찾지 못하면 탐색이 종료된다. (undefined 반환)

const buzz = {
flying:true
}

const woody = Object.create(buzz);
// 우디가 날 수 있게 되었다!
woody.flying; // true

woody는 buzz와 [[Prototype]]이 링크되었다. woody내부에는 flying이라는 프로퍼티가 없지만 연결된 buzz에서 해당 프로퍼티를 찾아 그 값을 반환한 것이다.

Untitled-d97e46ed-9a42-48ed-8157-084269d64d5c.png

for...in루프에서도 객체를 순회할 때 prototype 연결을 통해 탐색 가능한 프로퍼티라면 모두 열거한다.

    for(let i in woody){
        console.log(`${i}를 발견`)
    }
    // flying를 발견

그렇다면 객체를 객체와 연결해야 하는 이유는 뭘까? 그렇게 함으로써 어떤 장점이 있는걸까?

클래스 함수

    function Foo(){}

    const f = new Foo();
    Object.getPrototypeOf(a) === Foo.prototype; // true

함수를 정의하면 함수만 생성되는 것이 아니라 Prototype Object도 같이 생성이 된다.

Prototype Object는 기본속성으로 constructor__proto__를 가지고 있다. (따라서 정확히 말하자면 Foo.prototype객체가 프로토타입을 의미하는것은 아니다. constructor도 가지고 있으니! )

  • constructor는 Prototype Object와 같이 생성되었던 함수(내가 선언한 함수)를 가리킨다.(실재하는것 x) new 키워드와 함께 함수를 호출할 경우 생성자 호출을 하여 constructor함수(생성자 아님)를 실행하고 부수효과로 객체가 생성된다.
    생성자 함수가 아니라 함수를 생성하는 호출이라고 생각하는것이 맞다

  • proto는 [[Prototype]]링크이다.

Untitled-f837623b-c1d2-4ff0-ab2b-1fba6dd88352.png

new Foo()로써 만들어진 모든 객체(f)는 결국 Foo.prototype 객체와 내부적으로 [[Prototype]]링크로 연결된다.
결국 fFoo 는 상호 연결된 두개의 객체가 된다.

Untitled-2a35c021-b5ac-44aa-8880-43a0eb1f5480.png

객체지향언어에서 클래스를 인스턴스화 하기 위해 new 연산자를 사용한다. 그리고 자바스크립트에서도 두개의 객체를 연결하기 위해 new연산자를 사용한다.

하지만 전자의 경우 클래스로부터 작동을 복사하여 새로운 객체를 만드는 것이고, 후자의 경우 복사 과정 없이 그저 두 객체를 연결한 것이 전부이다.
따라서 자바스크립트의 new 키워드는 우리가 흔히 아는 new키워드와 다르게 동작한다는걸 알 수 있다.

js에서 new연산자는 결국 새 객체를 다른 객체와 연결하기 위한 간접적인 우회 방법이고, Object.create()를 이용해 직접적으로 객체를 연결해주는 방법도 있다.

[[Prototype]] 체계를 흔히들 프로퍼타입 상속이라고 부른다. 하지만 상속은 기본적으로 복사를 수반하지만, 자바스크립트는 객체 프로퍼티를 복사하지 않는다. 따라서 프로퍼타입 상속은 의미를 더 햇갈리게 만드는 단어의 조합이고, 위임이야말로 자바스크립트 객체-연결 체계를 훨씬 더 정확하게 나타내는 용어라고 할 수 있다.

생성자

앞 예제에서 new를 붙여 Foo함수를 호출하여 객체가 "생성"되는 현장을 목격했으니 Foo를 생성자라고 믿고 싶은 욕망을 버리기란 쉽지 않다.

하지만 Foo는 생성자가 아니다. 그저 함수일 뿐이다.

함수는 결코 생성자가 아니지만 new를 붙여 호출하는 순간 이 함수는 생성자 호출을 한다. new키워드는 일반 함수 호출 도중에 원래 수행할 작업 외에 객체 생성이라는 잔업을 더 부과하는 지시자인 것이다.

    function nothing(){
        console.log(`그저 함수입니다`)
    } 

    const a = new nothing();
    // "그저 함수입니다"
    a;
    // {}

평범한 함수인 nothing을 new로 호출함으로써 객체가 생성되고 부수효과로 생성된 그 객체를 a에 할당하는 것이다. 이것을 보통 생성자(constructor) 호출 이라고 부르지만 nothing함수 자체는 생성자가 아니다.

즉, 자바스크립트는 앞에 new를 붙여 호출한 함수를 모두 생성자라고 할 수 있다. 함수는 결코 생성자가 아니지만 new를 사용하여 호출할 때에만 생성자 호출이다.

정리


객체에 존재하지 않는 프로퍼티를 접근하려 시도하면 [[Get]]은 해당 객체의 내부 [[Prototype]] 링크를 따라 다음 수색 장소를 결정한다. 모든 일반 객체의 최상위 프로토타입 연쇄는 내장 Object.prototype이고 이 지점에서도 찾지 못하면 탐색이 종료된다.

두 객체를 서로 연결짓는 가장 일반적인 방법은 함수 호출 시 new키워드를 앞에 붙이는 것이다. new로 호출한 함수를 보통 생성자라고 일컫는데 클래스를 인스턴스화하는 생성자와는 개념이 전혀 다르다.

사실 자바스크립트 객체 간의 관계는 복사되는 게 아니라 위임 연결이 맺어진 것이므로 "위임"이라고 해야 더 적절한 표현이다.

클래스 비스무리한거 같기도 하고,, 뭐 상속? 받는다고 하던데,,, 프로토타입 체이닝도 중요하다고 하고...
그런데 이제 class문법 지원하는데 알아야 하나? 라는 생각이 들것 같기도 하다...(는 내가 그랬음)

참고