본문 바로가기
Development

[21.02.01] YDKJSY - Prototypes

by igy95 2024. 1. 8.

[[Prototype]]

모든 객체는 [[Prototype]] 체인으로 연결된 내부 프로퍼티를 가지고 있다. 이 말은 곧 객체 내에서 특정 프로퍼티를 검색하고자 할 때, 다른 객체에도 참조가 가능하다는 소리다. 기본적으로 모든 객체에는[[Get]] 연산이 있고 이것은 객체 내에서 프로퍼티를 참조할 때 사용되는데, 현재 객체 내에서 요청한 프로퍼티가 존재하지 않을 때 [[Prototype]] 링크를 따라가게 된다. 또한 반복문이나 기본적으로 키가 존재하는지 알아보기 위해 in 을 사용할 때에도 현재 객체에서만 탐색을 하는 것이 아니라 연결된 모든 객체를 참조하게 된다.

 

이렇듯 객체 내에서 탐색 관련 연산을 사용할 때는 체인의 끝까지 탐색을 하게 되는데, 그렇다면 [[Prototype]]의 끝은 어디일까? 그것은 바로 Object.prototype이라는 내장 객체이다.


이 객체는 JS에서 사용되는 다수의 메소드를 가지고 있기 때문에 모든 객체들이 이 내장 객체의 메소드를 [[Prototype]] 체인을 통해 상속 받는다.

Setting & Shadowing Properties

myObject.foo = "bar";

위의 연산을 기준으로 myObject를 현재 객체, 체인으로 연결되어있는 임의의 객체를 상위 객체로 보고 각 프로퍼티 foo 유무에 따른 결과를 알아 보자.

 

  • 현재 객체 X, 상위 객체 X : [[Prototype]] 체인 탐색 후 현재 객체에 프로퍼티 추가
  • 현재 객체 O, 상위 객체 X : 현재 객체에 참조 할당 바로 가능
  • 현재 객체 O, 상위 객체 O : 쉐도잉 룰에 따라 더 낮은 단계인 현재 객체에서만 참조 할당이 가능하다.

위의 결과와는 반대로, 현재 객체에서 존재하지 않고 상위 객체에서 프로퍼티가 존재할 때는 세 가지 시나리오를 예상해 볼 수 있다.

 

  1. foo 가 read-only(writable:false) 로 정의되지 않았을 때
    • foo라는 새로운 프로퍼티가 현재 객체에 바로 추가된다. (쉐도잉 룰에 따른 결과)
  2. foo 가 read-only로 정의 되었을 때
    • 존재하는 프로퍼티에 대한 설정과 현재 객체에 대한 새로운 프로퍼티 추가가 모두 제한된다.
    • strict mode 라면 에러 반환 / non-strict에서는 그냥 무시
  3. foo 가 상위 객체에서 setter로 존재할 때
    • setter가 항상 호출되고 해당 객체에는 프로퍼티가 추가되지 않고 setter도 재정의 될 수 없다.

위의 세가지를 토대로 여러 연산에 대해 조심해야겠지만, 일반적으로 나타날 수 있는 실수들을 예상해보자면 1번 케이스를 예시로 볼 수 있겠다. 상위 객체에 존재하는 프로퍼티의 값을 바꾸기 위해 현재 객체에서 무언가를 조작하면, 쉐도잉 룰에 따라 바뀐 값이 해당 객체 내에서 새로 추가된 동명의 프로퍼티에 할당되기 때문에 사전에 이런 실수를 하지 않도록 주의해야 할 것이다.

"Class"

그건 그렇고, 어째서 객체마다 링크나 체인이 형성되어야만 하는 걸까? 다른 이유들도 있겠지만, JS 내에서 클래스 설계 패턴을 구현하기 위해 필요한 기능이기 때문이다. 사실, JS는 클래스 없이 객체를 생성할 수 있기 때문에 '객체 지향' 언어로서는 매우 독특한 형태를 띄고 있다. 그렇기에 JS에서는 객체만 존재할 뿐, 클래스 관련 개념은 허상이라 생각할 수 있다.

"Class" Functions

기본적으로 모든 함수는 각자의 프로토타입 객체를 가지고 있다.

function Foo() {
    // ...
}

Foo.prototype; // { }

그리고 new Foo() 로 호출된 각각의 객체들은 Foo() 의 프로토타입에 연결된 [[Prototype]] 체인을 갖는다. 이러한 부분으로 보았을 때, 잠시 생각해 보아야할 부분이 있다.

 

정통의 클래스 지향 언어에서는 하나의 클래스에 대한 다수의 복사가 이루어진다. 이전에 다루었듯이, 이러한 동작이 발생하는 이유는 '클래스의 인스턴스화나 상속화의 과정은 해당 클래스로부터 동작들을 실질 객체에 복사하는 것'이기 때문이다.

 

하지만 JS에서는 복사가 수행되지는 않는다. 때문에 클래스에 대한 다수의 인스턴스를 생성할 수는 없다. 대신 공통 객체에 대한 [[Prototype]] 링크를 공유하는 여러 하위 객체들을 생성할 수 있는 것이다. 그리고 이러한 과정으로 생성된 객체들은 복사된 것이 아니기 때문에 상호 독립적이라고 볼 수 없다. 결국 JS에서는 new 키워드를 이용해 무언가를 생성해낼 때, 그 무언가는 결국 연결된 객체일 뿐 실제 클래스의 인스턴스는 아니다.

"Constructors"

function Foo() {
    // ...
}

Foo.prototype.constructor === Foo; // true

var a = new Foo();
a.constructor === Foo; // true

함수의 프로토타입 객체는 기본적으로 .constructor라는 프로퍼티를 갖는다. a 에서도 해당 프로퍼티를 확인할 수 있지만 이는 a 안에 실제로 존재하기 때문은 아니다. a 안에서 찾아볼 수 없기 때문에 Foo.prototype에 위임하게 되고 해당 프로퍼티를 참조하게 된다.

Constructor Or Call?

JS에서 new 키워드를 이용해 할당할 때, 해당 함수에서 새로운 객체가 반환되기 때문에 '생성자'로 볼 수 있지만, 사실 함수보다는 new 를 이용한 호출을 '생성자 호출'로 보는 것이 더 타당하다. 클래스 기능을 하는 함수들은 regular function 이고 그 함수를 새로운 변수에 할당할 때 어떤 식으로 호출하느냐에 따라 결과가 달라질 수 있기 때문이다.

Object Links

Create()ing Links

요약글에 전부를 다루어보지는 않았지만 new 키워드를 통해 생성한 Prototype link는 그 용도나 문법 때문에 여러 혼동을 가져올 가능성이 다분히 높다. 또한 함수가 아닌 단순 객체 간 연결을 할 때에도 어려움이 따르고 그 처리가 간결하지 못하다. 이에 대한 보완책으로 나온 것이 Object.create() 메소드이다. 해당 메소드는 괄호 안에 들어가는 객체를 공유하여 Prototype chain을 통해 프로퍼티의 위임이 가능하다.

Links As Fallbacks?

두 객체를 어떠한 방법을 사용했건 연결을 해주었다고 치자. 그럼 이제 하위 객체에서 호출하려는 메소드가 없을 때 그에 대한 대비책으로 Prototype link가 사용되었다면, 그것은 완벽한 방법이 되는 걸까? 저자는 그렇지 않다고 했다. 내가 작성해놓은 코드를 미래의 다른 개발자가 유지보수를 위해 살펴볼 때, 해당 객체에 존재하지 않는 메소드가 정상적으로 호출되고 작동되고 있다면 아마 적지 않은 혼동을 느낄 것이라는 게 그 이유다. 때문에 이 부분에 대해 '위임'을 살짝 설명해주었는데 자세한 건 다음 장에서 다루어 보겠지만, 일단 하나만 살펴 보자면

var anotherObject = {
    cool: function() {
        console.log( "cool!" );
    }
};

var myObject = Object.create( anotherObject );

myObject.doCool = function() {
    this.cool(); // internal delegation!
};

myObject.doCool(); // "cool!"

이렇게 현재 객체에 새로운 메소드를 생성해주고 그 메소드가 적절한 처리를 할 수 있도록 내부에서 위임을 해주는 것이 비교적 덜 혼란을 일으킬 것이라고 얘기를 했다. 이 부분에 대해서는 아직 공감은 잘 안되지만 차차 읽어보면 나의 주관을 더 키울 수 있지 않을까 싶다.

프로토타입 보충 링크