[프로토타입] - 프로토타입과 프로토타입 체인

Donggu(oo)·2022년 10월 26일
1

JavaScript

목록 보기
23/49
post-thumbnail

1. 프로토타입(prototype)


  • 자바스크립트의 모든 객체는 자신의 부모 역할을 하는 프로토타입(prototype)이라는 객체를 가지고 있으며, 객체 지향의 상속과 같이 모든 객체는 그 객체의 부모 객체인 프로토타입으로부터 프로퍼티와 메서드를 상속받는다.
  • 자바스크립트의 모든 객체는 최소한 하나 이상의 다른 객체로부터 상속을 받으며, 이 때 상속되는 정보를 제공하는 객체를 프로토타입(prototype)이라고 한다.

  • 아래 예제에서 kim과 park은 eyes와 nose를 공통적으로 가지고 있는데, 메모리에는 eyes와 nose가 두 개씩 총 4개 할당된다. 객체를100개 만들면 200개의 변수가 메모리에 할당되는 문제가 발생한다.
function Person() {
  this.eyes = 2;
  this.nose = 1;
}

var kim  = new Person();
var park = new Person();

console.log(kim.eyes);  // 2
console.log(kim.nose);  // 1
console.log(park.eyes); // 2
console.log(park.nose); // 1
  • Person.prototype이라는 빈 Object가 어딘가에 존재하고, Person 함수로부터 생성된 객체(kim, park)들은 어딘가에 존재하는 Object에 들어있는 값을 모두 가져다 쓸 수 있다. 즉, eyes와 nose를 어딘가에 있는 빈 공간에 넣어놓고 kim과 park이 공유해서 사용하는 것이다.
// 위 예제의 문제를 해결하기 위해 프로토타입을 사용한 예제
function Person() {}

Person.prototype.eyes = 2;
Person.prototype.nose = 1;

var kim  = new Person();
var park = new Person():

console.log(kim.eyes); // 2
...

1) prototype 프로퍼티

  • prototype 프로퍼티는 함수 객체만 가지고 있는 프로퍼티(constructor를 소유하는 프로퍼티)로, 생성자 함수가 생성하는 인스턴스의 프로토타입을 가리킨다.
// 함수 객체는 prototype 프로퍼티가 있음
function func() {}
func.hasOwnProperty('prototype') // true

// 일반 객체는 prototype 프로퍼티가 없음
const obj = {}
obj.hasOwnProperty('prototype') // false
  • prototype 프로퍼티를 사용하면 현재 존재하고 있는 프로토타입에 새로운 프로퍼티나 메서드를 추가할 수 있다.
function Dog(color, name, age) {
    this.color = color;
    this.name = name;
    this.age = age;
}
// 현재 존재하고 있는 Dog 프로토타입에 family 프로퍼티를 추가함.
Dog.prototype.family = "시베리안 허스키";
// 현재 존재하고 있는 Dog 프로토타입에 breed 메서드를 추가함.
Dog.prototype.breed = function () {
  return this.color + " " + this.family;
};

var myDog = new Dog("흰색", "마루", 1);  // {color: '흰색', name: '마루', age: 1}
var hisDog = new Dog("갈색", "콩이", 3); // {color: '갈색', name: '콩이', age: 3}

// 우리 집 강아지는 시베리안 허스키이고, 친구네 집 강아지도 시베리안 허스키입니다.
console.log("우리 집 강아지는 " + myDog.family + "이고, 친구네 집 강아지도 " + hisDog.family + "입니다.");
// 우리 집 강아지의 품종은 흰색 시베리안 허스키입니다.
console.log("우리 집 강아지의 품종은 " + myDog.breed() + "입니다.");
// 친구네 집 강아지의 품종은 갈색 시베리안 허스키입니다.
console.log("친구네 집 강아지의 품종은 " + hisDog.breed() + "입니다.");

2) constructor(생성자 함수)

  • constructor(생성자)class로 생성된 인스턴스 객체를 생성하고 초기화하기 위한 특수한 메서드이다.
  • 클래스 내에서 생성자 함수는 하나만 존재할 수 있다.
  • 디폴트 프로퍼티 prototypeconstructor 프로퍼티 하나만 있는 객체를 가리키는데, 여기서 constructor 프로퍼티는 함수 자신을 가리킨다.
  • 생략이 가능하여 생략시 빈 constructor, 즉 빈 객체가 생성된다.
  • 생성자 함수를 작성하지 않으면, 기본 생성자(default constructor)가 제공되며, 기본(base) 클래스일 경우는 기본 생성자는 비어있으며, 파생(derived) 클래스일 경우 기본 생성자는 부모 생성자를 부른다.
// class를 선언하여 Computer라는 이름의 빈 객체를 생성했다.
class Computer {
  
}

let cpu = new Computer()
console.log(cpu)  // Computer {}

3) __proto__ / [[Prototype]] 프로퍼티

  • 모든 자바스크립트 객체는 자신의 프로토타입을 가리키는 [[Prototype]]을 가지고 있으며, __proto__ 접근자 프로퍼티를 통해 자신의 프로토타입인 [[Prototype]] 내부 슬롯에 접근하여 상위 객체 역할을 하는 생성자 함수의 prototype 객체의 내부에 정의된 모든 프로퍼티와 메서드를 사용할 수 있다.

[[Prototype]] : 내부 슬롯(internal slot)

  • 자바스크립트의 객체는 명세서에서 명명한 [[Prototype]]이라는 숨김 프로퍼티를 갖는다. 이 숨김 프로퍼티 값은 null이거나 다른 객체에 대한 참조가 되는데, 다른 객체를 참조하는 경우 참조 대상을 '프로토타입(prototype)'이라 부른다.

2-1. __proto__는 접근자 프로퍼티이다.

  • 원칙적으로 [[prototype]]는 내부 슬롯이므로 프로퍼티가 아니고, 내부 슬롯이나 내부 메서드에 직접 접근하거나 호출할 수는 없다. 따라서 __proto__ 접근자 프로퍼티를 통해 간접적으로 [[prototype]]의 값, 즉 상위 객체의 프로토타입에 접근할 수 있다.
  • __proto__[[Prototype]]에 접근하기 위한 수단이지 [[Prototype]] 그 자체가 아니다.
  • __proto__[[Prototype]]getter(획득자)이자 setter(설정자) 이다.
const obj = {};
const parent = {
  x: 1
};

// `__proto__` 접근자 프로퍼티로, 프로토타입에 접근하면 getter 함수인 `[[Get]]` 호출
// getter 함수가 호출되어 객체의 프로토타입 얻음
console.log(obj.__proto__); // [Object: null prototype] {}

// __proto__` 접근자 프로퍼티로, 새로운 프로토타입을 할당하면 setter 함수인 `[[Set]]` 호출
// setter 함수가 호출되어 객체의 프로토타입 교체
obj.__proto__ = parent;  // 상속받은 객체의 내용 변경

console.log(obj.__proto__); // { x: 1 }
console.log(obj.x); // 1
  • 인스턴스의 __proto__접근자 프로퍼티가 가리키는 객체와 해당 인스턴스를 생성한 생성자 함수의 prototype 프로퍼티가 가리키는 객체는 같다.
class Human {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }

  sleep() {
    console.log(`${this.name}은 잠에 들었습니다`);
  }
}

let kimcoding = new Human('김코딩', 30);  // 인스턴스

Human.prototype.constructor === Human;  // true
// 생성된 인스턴스인 kimcoding에는 __proto__라는 속성이 자동으로 부여되고,
// __proto__ 속성은 생성자 함수 Humandml prototype 속성을 참조한다.
Human.prototype === kimcoding.__proto__;  // true 
Human.prototype.sleep === kimcoding.sleep;  // true

2-2. __proto__는 상속을 통해 사용된다.

  • __proto__ 접근자 프로퍼티는 객체가 직접 소유하는 프로퍼티가 아니라 Object.prototype의 프로퍼티다. 모든 객체는 상속을 통해 Object.prototype.__proto__ 접근자 프로퍼티를 사용할 수 있다.
const person = { name: 'Lee' };

// person 객체는 __proto__ 프로퍼티를 소유하지 않는다.
console.log(person.hasOwnProperty('__proto__')); // false

// __proto__ 프로퍼티는 모든 객체의 프로토타입 객체인 Object.prototype의 접근자 프로퍼티다.
console.log(Object.getOwnPropertyDescriptor(Object.prototype, '__proto__'));
// {get: ƒ, set: ƒ, enumerable: false, configurable: true}

// 모든 객체는 Object.prototype의 접근자 프로퍼티 __proto__를 상속받아 사용할 수 있다.
console.log({}.__proto__ === Object.prototype); // true

2-3. __proto__를 통해 프로토타입에 접근하는 이유

  • __proto__를 굳이 사용하여 [[prototype]]의 값에 접근하는 이유는 상호 참조에 의해 프로토타입 체인이 생성되는 것을 방지하기 위해서다.
  • 아래 예제 처럼 두 객체가 서로의 프로토타입으로 설정하는 것이 에러 없이 정상적으로 작동한다면 비정상적인 프로토타입 체인이 만들어지기 때문에, __proto__는 에러를 발생시킨다.
const parent = {};
const child = {};

// child의 프로토타입을 parent로 설정
child.__proto__ = parent;
// parent의 프로토타입을 child로 설정
parent.__proto__ = child; // TypeError: Cyclic __proto__ value
  • 프로토타입 체인은 무조건 단방향 리스트로 구현되야 한다. 즉, 프로퍼티 검색 방향이 한쪽 방향으로만 흘러가야 한다. 만약 서로 참조하는 순환형 프로토타입 체인이 만들어지게 된다면 프로토타입 체인의 종점이 존재하지 않기 때문에 프로퍼티를 검색할 때 무한루프에 빠지게 된다. 따라서 아무런 체크 없이 무조건적으로 프로토타입을 교체할 수 없도록 __proto__를 이용해 프로토타입을 교체하도록 구현했다.

2-4. __proto__ 사용은 권장하지 않는다.

  • __proto__는 ES5까지는 ECMAScript 사양에 포함되지 않은 비표준이었다. 하지만 일부 브라우저에서 지원하기 때문에 호환성을 고려하여 ES6에서 __proto__를 표준으로 채택했다. 다만, 아래 예제처럼 모든 객체가 __proto__를 사용할 수 있는 것은 아니기 때문에 실제 코드 내에서 __proto__를 직접 사용하는 것은 권장하지 않는다.
// obj는 프로토타입 체인의 종점이다. 따라서 Object.__proto__를 상속받을 수 없다.
const obj = Object.create(null);

// obj는 Object.__proto__를 상속받을 수 없다.
console.log(obj.__proto__); // undefined
  • 따라서 __proto__대신하여 아래의 메서드를 사용한다.
    • Object.getPrototypeOf(obj) : obj의 [[prototype]]의 값을 참조한다.
    • Object.setPrototypeOf(obj) : obj의 [[Prototype]]이 proto가 되도록 교체한다.

2. 프로토타입 체인(prototype chain)


  • 프로토타입 체인은 __proto__의 특징을 이용하여, 부모 객체의 프로퍼티나 메서드를 차례로 검색하는 것을 의미한다. 즉, 특정 객체의 프로퍼티나 메서드 접근 시 자신의 것 뿐만 아니라 부모 객체의 것에도 접근하여 사용가능하다는 것을 말한다.
  • 현재 객체의 __proto__ 프로퍼티를 참조해서 해당 프로퍼티가 있는지 체크하고, 그래도 없으면 부모의 __proto__ 프로퍼티를 참조해서 해당 프로퍼티를 체크하는 것을 말한다.
  • 모든 프로토타입 체인의 종점은 Object.prototype이고, 최종 Object.prototype객체까지 해당하는 프로퍼티가 존재하지 않는다면 undefined를 반환한다.
// 생성자 Sub를 통해서 만들어진 객체 hero가 Ultra의 프로퍼티 ultraProp에 접근 가능한 것은
// Prototype 체인으로 Sub와 Ultra가 연결되어 있기 때문이다.
function Ultra(){}
Ultra.prototype.ultraProp = true;  // 4. 없다면 Ultra.prototype.ultraProp를 찾는다.
 
function Super(){}
Super.prototype = new Ultra();  // 3. 없다면 Super.prototype.ultraProp를 찾는다.
 
function Sub(){}
Sub.prototype = new Super();  // 2. 없다면 Sub.prototype.ultraProp를 찾는다.
 
var hero = new Sub();
console.log(hero.ultraProp);  // 1. 객체 hero에서 ultraProp를 찾는다.

1) 메서드 오버라이딩(method overridng)

  • 오버라이딩이란 자바스크립트 객체의 상속받은 부모의 메서드를 자식 클래스에서 재정의(덮어씌우기)하는 것을 의미한다.(오버라이딩이라는 개념은 존재하지만 자바같은 언어와는 다르다)
  • 원본을 제거하고 다른 대상으로 교체하는 것이 아닌, 원본이 그대로 있는 상태에서 다른 대상을 그 위에 덮어씌운 상황을 말한다.
// 부모 클래스
class Parent {
  constructor (name) {
    this.name = name;
  }
  who () {
    return this.name + ' is father';
  }
}

const parent = new Parent('Tom');
console.log(parent.who);  // 'Tom is father'

// 자식 클래스
class Child extends Parent {
  constructor(name) {
    super(name);
  }
  who () {  // return super.who;
    return this.name + ' is child';  // 오버라이딩
  }
}

const child = new Child('Max');
console.log(child.who);  // 'Max is child'

0개의 댓글