본문 바로가기

자바스크립트

[모던자바스크립트 Deep Dive] 19. 프로토타입(1)

자바스크립트는 프로토타입 기반의 객체지향 프로그래밍 언어이며 자바스크립트를 이루고 있는 거의 '모든 것'이 객체다.

 

1. 객체지향 프로그래밍

객체지향 프로그래밍 : 절차지향적 관점에서 벗어나 여러 개의 독립적 단위인 객체의 집합으로 프로그램을 표현하려는 프로그래밍 패러다임

 

  • 객체지향 프로그래밍은 실세계의 실체를 인식하는 철학적 사고를 프로그래밍에 접목하려는 시도에서 시작
  • 실체는 특징이나 성질을 나타내는 속성을 가지고 있음
  • 객체 : 상태 데이터와 동작을 하나의 논리적인 단위로 묶은 복합적인 자료구조
  • 추상화 : 다양한 속성 중에서 필요한 속성만 간추려 표현하는 것

 

2. 상속과 프로토타입

상속 : 어떤 객체의 프로퍼티 또는 메서드를 다른 객체가 상속받아 그대로 사용할 수 있는 것

 

자바스크립트는 프로토타입을 기반으로 상속을 구현하여 불필요한 중복을 제거
// 생성자 함수
function Circle(radius) {
  this.radius = radius;
}

// Circle 생성자 함수가 생성한 모든 인스턴스가 getDiameter 메서드를
// 공유해서 사용할 수 있도록 프로토타입에 추가한다.
// 프로토타입은 Circle 생성자 함수의 prototype 프로퍼티에 바인딩되어 있다.
Circle.prototype.getDiameter = function () {
  return Math.PI * this.radius ** 2;
};

// 인스턴스 생성
const circle1 = new Circle(1);
const circle2 = new Circle(2);

// Circle 생성자 함수가 생성한 모든 인스턴스는 부모 객체의 역할을 하는
// 프로토타입 Circle.prototype으로부터 getDiameter 메서드를 상속받는다.
// 즉, Circle 생성자 함수가 생성하는 모든 인스턴스는 하나의 getDiameter 메서드를 공유한다.
console.log(circle1.getDiameter === circle2.getDiameter); // true

console.log(circle1.getDiameter()); // 3.141592653589793
console.log(circle2.getDiameter()); // 12.566370614359172

위 예제와 같이 Circle 생성자 함수가 생성한 모든 인스턴스는 자신의 프로토타입인 Circle.prototype의 모든 프로퍼티와 메서드를 상속받는다. 그렇게 때문에 Circle 생성자 함수가 생성하는 모든 인스턴스는 getDiameter함수를 상속받아 사용할 수 있다. 이는 코드 재사용 면에서 매우 유리하다.

상속에 의한 메서드 공유

 

 

3. 프로토타입 객체

  • 프로토타입 객체 : 객체지향 프로그래밍의 근간을 이루는 객체 간 상속을 구현하기 위해 사용
  • 프로토타입 : 어떤 객체의 상위 객체의 역할을 하는 객체
    • 다른 객체에 공유 프로퍼티를 제공
    • 프로토타입을 상속받은 하위 객체는 상위 객체의 프로퍼티를 자신의 프로퍼티처럼 자유롭게 사용할 수 있다.
  • 모든 객체는 [[Prototype]]이라는 내부 슬롯을 가지며, 이 내부 슬롯의 값은 프로토타입의 참조이다.
  • [[Prototype]]에 저장되는 프로토타입은 객체 생성 방식에 의해 결정된다.
        => 객체가 생성될 때 객체 생성 방식에 따라 프로토타입이 결정되고 [[Prototype]]에 저장
  • 모든 객체는 하나의 프로토타입을 갖는다.
  • 모든 프로토타입은 생성자 함수와 연결되어 있다.

 

1. __proto__ 접근자 프로퍼티

모든 객체는 __proto__ 접근자 프로퍼티를 통해 자신의 프로토타입인 [[Prototype]] 내부 슬롯에 간접적으로 접근할 수 있다.

__proto__ 캡처

이렇게 모든 객체는 __proto__ 접근자 프로퍼티를 통해 프로토타입을 가리키는 [[Prototype]] 내부 슬롯에 접근할 수 있다.

 

  • __proto__는 접근자 프로퍼티다
    • 내부 슬롯은 프로퍼티가 아니므로 원칙적으로 내부 슬롯과 내부 메서드에 직접적으로 접근하거나 호출할 수 있는 방법을 제공하지 않는다.
    • 일부 내부 슬롯과 내부 메서드에 한해 간접적으로 접근할 수 있다.
  • __proto__ 접근자 프로퍼티는 상속을 통해 사용된다.
    • 객체가 직접 소유하는 프로퍼티가 아니라 Object.prototype의 프로퍼티이다.
      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​
  • __proto__ 접근자 프로퍼티를 통해 프로토타입에 접근하는 이유
    • => 상호 참조에 의해 프로토타입 체인이 생성되는 것을 방지하기 위해서
  • __proto__ 접근자 프로퍼티를 코드 내에서 직접 사용하는 것은 권장하지 않는다.
    • 모든 객체가 __proto__ 접근자 프로퍼티를 사용할 수 있는 것은 아니기 때문에
      // obj는 프로토타입 체인의 종점이다. 따라서 Object.__proto__를 상속받을 수 없다.
      const obj = Object.create(null);
      
      // obj는 Object.__proto__를 상속받을 수 없다.
      console.log(obj.__proto__); // undefined
      
      // 따라서 Object.getPrototypeOf 메서드를 사용하는 편이 좋다.
      console.log(Object.getPrototypeOf(obj)); // null​
    • 프로토타입의 참조를 취득하고 싶은 경우에는 Object.PrototypeOf 메서드를 사용
    • 프로토타입을 교체하고 싶은 경우에는 Object.setPrototypeOf 메서드를 사용
      const obj = {};
      const parent = { x: 1 };
      
      // obj 객체의 프로토타입을 취득
      Object.getPrototypeOf(obj); // obj.__proto__;
      // obj 객체의 프로토타입을 교체
      Object.setPrototypeOf(obj, parent); // obj.__proto__ = parent;
      
      console.log(obj.x); // 1​

 

2. 함수 객체의 prototype 프로퍼티

함수 객체만이 소유하는 prototype 프로퍼티는 생성자 함수가 생성할 인스턴스의 프로토타입을 가리킨다.

// 함수 객체는 prototype 프로퍼티를 소유한다.
(function () {}).hasOwnProperty('prototype'); // -> true

// 일반 객체는 prototype 프로퍼티를 소유하지 않는다.
({}).hasOwnProperty('prototype'); // -> false
  • non-constructor인 화살표 함수와 ES6 메서드 축약 표현으로 정의한 메서드는 prototype 프로퍼티를 소유하지 않고 프로토타입도 생성하지 않는다.
  • 모든 객체가 가지고 있는 __proto__ 접근자 프로퍼티와 함수 객체만 가지고 있는 prototype 프로퍼티는 동일한 프로토타입을 가리킨다.
    // 생성자 함수
    function Person(name) {
      this.name = name;
    }
    
    const me = new Person('Lee');
    
    // 결국 Person.prototype과 me.__proto__는 결국 동일한 프로토타입을 가리킨다.
    console.log(Person.prototype === me.__proto__);  // true​

 

3. 프로토타입의 constructor 프로퍼티와 생성자 함수

  • 모든 프로토타입은 constructor 프로퍼티를 갖는다.
  • constructor 프로퍼티는 prototype 프로퍼티로 자신을 참조하고 있는 생성자 함수를 가리킨다.

프로토타입의 constructor 프로퍼티

위 예시에서 Person 생성자 함수는 me 객체를 생성했다. 이 때, me 객체는 프로토타입의 constructor 프로퍼티를 통해 생성자 함수와 연결된다. 결국, me 객체에는 constructor 프로퍼티가 없지만 그의 프로토타입인 Person.prototype에는 constructor 프로퍼티가 있으므로 상속받아 사용할 수 있다.