본문 바로가기
  • soldonii's devlog
Javascript 공부/TIL

200110(금) : 프로토타입

by soldonii 2020. 1. 10.

오늘은 프로토타입에 대해 정리한다.

1. 프로토타입

# [[Prototype]]

  • 자바스크립트 객체는 [[Prototype]]이라는 내부 프로퍼티를 가지고 있으며, 이 녀석은 다른 객체를 참조하는 단순한 레퍼런스로 사용된다.
  • [[Prototype]] 링크는 현재 객체(obj)에서 찾고자 하는 프로퍼티(a)를 찾지 못할 경우, obj의 프로토타입 링크를 통해 프로토타입 체인을 거슬러 올라가면서 프로퍼티 a를 수색할 수 있도록 한다.
  • 이 연쇄를 따라 올라가면서 프로퍼티 a를 찾고, 최상단에서도 찾지 못할 경우 undefined를 리턴하게 된다.
  • 또한 for ... in 루프로 객체를 순회할 때에도 [[Prototype]] 링크를 통해 객체 연쇄를 전부 찾는다.
  • 모든 자바스크립트 객체는 최상단의 Object.prototype의 자식이다. 따라서 [[Prototype]] 연쇄는 최상단의 Object.prototype까지 올라가게 된다.
  • 만약 obj.foo = 123으로 객체 objfoo 프로퍼티를 추가하려고 하는데, 만약 obj의 프로토타입 체인의 상위에 foo라는 프로퍼티가 존재한다면, 현재 객체 objfoo 프로퍼티가 선언됨으로써 상위의 foo 프로퍼티는 영원히 가려지게 된다.

 

# 클래스 함수

  • new 키워드를 이용해 호출된 생성자 함수로 생성된 인스턴스는 프로토타입 객체와 [[Prototype]] 링크로 연결이 된다.
function Foo() {
  // ...
}

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

 

 

  • new Foo()로 새로운 객체(a)가 생성되며, 이 객체는 Foo.prototype 객체와 내부적으로 [[Prototype]] 연결이 맺어진다.
  • 객체 aFoo.prototype은 단지 서로 연결될 뿐이다. 같은 객체도 아니고, Foo.prototype 객체가 a에 복사되는 것도 아니다. 개별적인 두 개의 객체이다.
  • 결국 new Foo()는 새로운 객체를 다른 객체와 연결짓기 위한 우회 방법 중 하나이며, 더 직접적으로는 Object.create()를 사용할 수 있다.
  • [[Prototype]] 체계를 다른 말로 '프로토타입 상속'이라고 부르는데, 이는 엄밀히 잘못된 표현이다.
  • 상속은 기본적으로 복사를 수반한다. 즉, 상위 객체의 프로퍼티를 하위 객체에 복사한다는 의미인데, 자바스크립트에서 [[Prototype]] 링크는 그렇게 작동하지 않는다.
  • 단지 a와 b 객체 간 연결만 지어줄 뿐이다. 따라서 '상속' 대신 '위임'이라고 부르는 것이 정확하다.

 

# 생성자

  • 생성자 : 위의 코드 사례를 예로 들자면, Foo.prototype 객체에는 기본적으로 열거 불가능한 공용 프로퍼티 .constructor가 세팅된다. 이는 객체 생성과 관련된 함수(Foo)를 다시 참조하기 위한 레퍼런스이다.
  • 함수는 생성자가 아니지만, new를 붙여 사용할 때에만 '생성자 호출'을 한다. new로 생성자 호출을 하면 함수에 원래 해야할 작업 외 객체 생성이라는 추가 작업을 지시하게 된다.

 

# 생성자와 프로토타입의 체계

function Foo(name) {
  this.name = name;
}

Foo.prototype.myName = function () {
  return this.name;
};

let a = new Foo('a');
let b = new Foo('b');

a.myName(); // 'a'
b.myName(); // 'b'

 

 

 

function Foo() {
  // ...
}

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

 

  • a.constructor === Foo가 맞다고 해서, a 객체 내부에 constructor 프로퍼티가 존재한다고 생각하면 틀렸다.
  • 단지 [[Prototype]] 위임을 통해서 전달받았을 뿐이다.
  • Foo.prototype.constructor 프로퍼티는 기본으로 선언된 Foo 함수에 의해 생성된 객체에만 존재한다.

 

# 프로토타입 상속

function Foo(name) {
  this.name = name;
}
function Bar(name, label) {
  Foo.call(this, name);
  this.label = label;
}

Bar.prototype = Object.create(Foo.prototype);

 

  • Object.create()는 '새로운' 객체를 생성한 뒤, 내부의 [[Prototype]]을 인자로 전달한 객체에 링크하는 메소드이다.
  • Bar.prototype = Foo.prototype처럼 할당하면 안된다. 이는 단지 Bar.prototypeFoo.prototype 객체를 가리키는 레퍼런스로 만들어, 사실상 Foo에 링크된 Foo.prototype 객체에 직접 연결한다.
  • 따라서 Bar.prototype.myLabel = ... 와같은 할당문은 Foo.prototype 자체를 변경하게 되어 이와 연결된 모든 객체에 악영향을 미친다.
  • ES6 이후에는 Object.setPrototypeOf()를 이용해서 기존 객체의 연결 정보를 수정할 수 있게 되었다.(Object.create()는 기존 연결정보를 수정하는 것이 아니라, 리턴되는 새로운 객체로 아예 대체시켜버린다.)

 

// ES6 이전, Object.create()
Bar.prototype = Object.create(Foo.prototype);

// ES6 이후, Object.setPrototypeOf()
Object.setPrototypeOf(Bar.prototype, Foo.prototype);

 

# 클래스 관계 조사

 

  • 한 객체가 어떠한 객체를 위임받고 있는지를 알 수 있는 방법을 살펴보자.
  • a instanceof Foo : 이는 a의 [[Prototype]] 연쇄를 순회하면서 Foo.prototype이 가리키는 객체가 있는지 조사한다.
  • 하지만 instanceof는 대상 함수(Foo)에 대해 주어진 객체(a)의 계통만을 살펴볼 수 있다는 한계가 있다. 만약 2개의 객체(a, b)가 있을 경우, 두 객체가 서로 [[Prototype]] 연쇄로 연결되어 있는지는 알 수 없다.
  • 더 훌륭한 대안으로 isPrototypeOf()를 사용할 수 있다.
  • Foo.prototype.isPrototypeOf(a) : 이는 a의 전체 [[Prototype]] 연쇄에 Foo.prototype이 존재하는지를 확인한다.
  • 또한 함수 대신 b.isPrototypeOf(c)처럼 두 객체 간에 [[Prototype]] 연쇄가 존재하는지도 확인할 수 있다.
  • Object.getPrototypeOf(a) : a 객체의 [[Prototype]]을 조회하여, a에게 프로퍼티를 위임하고 있는 Foo.prototype를 조회할 수 있다.

 

# [[Prototype]] 링크는 대비책이 아니다.

  • 만일 회사에서 개발할 때, myObject.cool() 이란 메소드를 호출할 때, myObjectcool 프로퍼티가 존재하지 않아도 정상적으로 동작하도록 설계되었다면, 추후 유지보수에 매우 어려움을 줄 수 있다. myObject의 상위 링크에서 cool 프로퍼티를 찾았기 때문에 동작했겠지만 이를 명시적으로 드러내지 않을 경우 오류를 초래할 수 있다.
  • 대신 아래처럼 명시적으로 상위 링크의 프로퍼티를 위임받는 '내부 위임' 격의 코드를 작성하는 것이 좋다.
let anotherObj = {
  cool: function() {
    console.log('cool');
  }
};

let myObj = Object.create(anotherObj);
myObj.doCool = function () {
  this.cool(); // 내부 위임
};

myObj.doCool();

댓글