Skip to content

Latest commit

 

History

History
801 lines (489 loc) · 36.4 KB

15. Prototype and Inheritance.md

File metadata and controls

801 lines (489 loc) · 36.4 KB

15. Prototype and Inheritance

  • 자바스크립트는 프로토타입을 기반으로 한 객체지향 프로그래밍 언어(prototype-based oop)이다.
  • 자바스크립트에서 원시 값을 제외한 모든 것이 객체이다.

이 문서에서는 Class로 생성된 객체는 다루지 않는다.

15.1 프로토타입 기반 상속

  • 동일한 생성자 함수에 의해 생성된 모든 인스턴스가 동일한 프로퍼티를 중복하여 가지는 것은 메모리와 성능 면에 비효율적이다. 객체지향 프로그래밍 언어는 상속(inheritance)을 통하여 불필요한 중복을 제거한다.
  • 자바스크립트는 프로토타입을 기반으로 상속을 구현한다. 모든 인스턴스는 프로토타입의 프로퍼티를 상속을 통해 공유한다.
function Champion(name) {
    this.name = name;
}

Champion.prototype.printName = function () {
    console.log(this.name);
};

const c1 = new Champion('lux');
const c2 = new Champion('azir');

c1 === c2;	// false
c1.printName === c2.printName;	// true

Champion의 인스턴스 c1c2는 다른 객체이지만 같은 메서드를 공유한다.

15.2 프로토타입 객체

  • 프로토타입(prototype; 프로토타입 객체)은 상위 객체로서 다른 객체에 공유 프로퍼티를 제공한다. 즉, 프로퍼티를 상속한다. 프로퍼티를 상속받은 객체는 하위 객체로서 상위 객체의 프로퍼티를 사용할 수 있다.
  • 모든 객체는 단 하나의 프로토타입을 가지며, 모든 프로토타입은 생성자 함수와 연결되어 있다.

[[Prototype]]

모든 객체는 프로토타입을 가지며, 이 프로토타입은 내부 슬롯 [[Prototype]]에 저장된다.

  • 모든 객체는 내부 슬롯 **[[Prototype]]**을 가지며, [[Prototype]]은 프로토타입의 참조를 값으로 가진다. 이때 저장되는 프로토타입은 객체를 생성하는 방식에 따라 달라진다.
  • 모든 객체는 Object.prototype.__proto__ 접근자 프로퍼티를 통해 [[Prototype]]이 가리키는 프로토타입에 간접적으로 접근할 수 있다.

Object.prototype.__proto__

  • 모든 객체는 __proto__ 접근자 프로퍼티를 통해 자신의 프로토타입에 간접적으로 접근할 수 있다.
function Champion(name) {
    this.name = name;
}

const champion = new Champion('unknown');
champion.__proto__;	// { constructor: ƒ }

__proto__ 접근자 프로퍼티를 통해 프로토타입에 접근하면 내부적으로 __proto__ 접근자 프로퍼티의 getter 함수인 [[Get]]이 호출된다. __proto__ 접근자 프로퍼티를 통해 새로운 프로퍼티를 할당하면 __proto__ 접근자 프로퍼티의 setter 함수인 [[Set]]이 호출된다.

  • 모든 객체는 상속을 통하여 Object.prototype.__proto__ 접근자 프로퍼티를 사용할 수 있다.

__proto__ 접근자 프로퍼티를 사용하는 이유

  • 프로토타입 체인은 단방향 연결 리스트로 구현되어 프로퍼티의 검색 방향은 한쪽 방향으로만 흐른다. 두 개의 서로 다른 프로토타입이 서로를 참조하면 순환 참조(circular reference)하는 프로토타입 체인이 만들어져 프로토타입 검색이 무한 루프에 빠지게 되기 때문이다.
  • 따라서 __proto__ 접근자 프로퍼티를 사용하여 상호 참조하는 경우 TypeError가 발생하도록 한다.
const parent = {};
const child = {};

child.__proto__ = parent;
parent.__proto__ = child;	// TypeError: Cyclic __proto__ value

Object.getPrototypeOf 메서드와 Object.setPrototypeOf 메서드 사용하기

__proto__ 프로퍼티는 브라우저 호환성을 고려하여 ES6부터 표준으로 채택된 사양이다. 또한, 객체가 Object.prototype을 상속받지 않는다면 __proto__ 접근자 프로퍼티를 사용할 수 없다. 예를 들어 프로토타입의 종점은 종점으로서 어떤 프로퍼티도 상속받지 않는다.

// obj는 프로토타입의 종점으로 Object.__proto__를 상속받지 않는다.
const obj = Object.create(null);
console.log(obj.__proto__);	// undefined

이러한 이유로 __proto__ 접근자 프로퍼티 대신 다음 메서드를 사용한다.

  • Object.getPrototypeOf(ES5): 프로토타입의 참조를 반환한다. get Object.prototype.__proto__와 동작이 일치한다.
  • Object.setPrototypeOf(ES6): 프로토타입을 교체한다. set Object.prototype.__proto__와 동작이 일치한다.
const obj = Object.create(null);
console.log(Object.getPrototypeOf(obj));	// null

constructor 함수 객체의 prototype 프로퍼티

constructor 함수 객체(함수 선언문, 함수 표현식, 클래스로 선언된 함수)만 소유하는 prototype 프로퍼티는 생성자 함수가 생성할 인스턴스의 프로토타입을 가리킨다. 즉, constructor인 함수만이 프로토타입을 생성할 수 있다.

/* constructor 함수 객체는 prototype 프로퍼티를 가진다 */
(function() {}).hasOwnProperty('prototype');	// true
(function() {}).prototype;	// {constructor: ƒ}

/* non-constructor 함수 객체는 prototype 프로퍼티를 가지지 않는다 */
(() => {}).hasOwnProperty('prototype');		// false
(() => {}).prototype;	// undefined

__proto__ 접근자 프로퍼티와 prototype 프로퍼티의 비교

임의의 생성자 함수에 대하여, 생성자 함수의 prototype 프로퍼티와 생성자 함수로 생성한 인스턴스의 __proto__는 같은 프로토타입 객체를 참조한다.

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

const champion = new Champion('rammus');
console.log(Champion.prototype === champion.__proto__);	// true

그러나 두 프로퍼티를 사용하는 주체가 다르다.

구분 소유 사용 주체 사용 목적
__proto__ 접근자 프로퍼티 모든 객체(Object.prototype으로부터 상속받는다) 프로토타입의 참조 모든 객체 객체가 자신의 프로토타입에 접근하기 위해 사용한다.
prototype 프로퍼티 constructor인 함수 객체 프로토타입의 참조 생성자 함수 생성자 함수가 자신이 생성할 인스턴스의 프로토타입을 할당하기 위해 사용한다.

프로토타입 객체의 constructor 프로퍼티

모든 프로토타입 객체는 constructor 프로퍼티를 가진다. constructor 프로퍼티는 prototype 프로퍼티로 자신을 참조하는 생성자 함수를 참조한다. 자바스크립트 엔진은 생성자 함수가 생성될 때 constructor프로퍼티와 prototype 프로퍼티에 참조를 할당한다.

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

const rammus = new Champion('rammus');
console.log(rammus.constructor === Champion);	// true

rammus에는 constructor 프로퍼티가 없지만 rammus의 프로토타입인 Champion.prototype에 있는 constructor 프로퍼티를 상속받아 사용할 수 있다.

__proto__, prototype, constructor 프로퍼티의 비교

구분 참조 소유
Object.prototype.__proto__ 객체의 프로토타입을 가리킨다 모든 객체
Object.prototype 생성자 함수에 바인딩된 프로토타입을 가리킨다 생성자 함수 객체
Object.prototype.constructor 프로토타입에 바인딩된 생성자 함수를 가리킨다 프로토타입 객체
Object === Object.prototype.constructor;	// true
Object.prototype === new Object().__proto__;	// true
Object.prototype.__proto__ === null;	// true
Object.__proto__ === Function.prototype;	// true
function Foo () {}

Foo === Foo.prototype.constructor;	// true
Foo.prototype === new Foo().__proto__;	// true
Foo.prototype.__proto__ === Object.prototype;	// true
Foo.__proto__ === Function.prototype;	// true
  • 생성자 함수는 prototype 프로퍼티를 통해 바인딩된 프로토타입(자신이 생성할 인스턴스의 프로토타입)에 접근할 수 있다.
  • 프로토타입 객체는 constructor 프로퍼티를 통해 바인딩된 생성자 함수에 접근할 수 있다.
  • 모든 객체는 __proto__ 프로퍼티를 통해 프로토타입에 접근할 수 있다.

15.3 프로토타입의 생성 시점

  • 프로토타입은 생성자가 생성되는 시점에 함께 생성된다. 즉, 프로토타입과 생성자는 단독으로 존재할 수 없고 항상 같이 존재한다.
  • 생성자와 생성자에 연결된 프로토타입의 생성 시점은 런타임 이전이다.
    • 빌트인 생성자 함수와 그 프로토타입의 생성 시점: 전역 객체가 생성되는 시점(런타임 이전)
    • 사용자 정의 생성자 함수와 그 프로토타입의 생성 시점: 함수 정의가 평가되는 시점(런타임 이전)

빌트인 생성자 함수와 프로토타입의 생성 시점

  1. 빌트인 생성자 함수 객체와 프로토타입 객체의 생성: 빌트인 생성자 함수는 전역 객체가 생성되는 시점 곧 런타인 이전에 생성된다. 이때 프로토타입도 함께 생성된다.
  2. 프로퍼티 바인딩
    • 생성자의 prototype 프로퍼티에 프로토타입이 바인딩된다.
    • 프로토타입의 constructor 프로퍼티에 생성자가 바인딩된다.

사용자 정의 생성자 함수와 프로토타입의 생성 시점

  1. 생성자 함수 객체와 프로토타입 객체의 생성: 함수 호이스팅에 의해 함수 정의가 평가되어 함수 객체가 생성되는 시점은 런타임 이전이다. 따라서 constructor 역시 함수 정의가 평가되어 함수 객체가 생성되는 시점에 생성된다. 이때 프로토타입도 함께 생성된다.
  2. 프로퍼티 바인딩
    • 생성자의 prototype 프로퍼티에 프로토타입이 바인딩된다.
    • 프로토타입의 constructor 프로퍼티에 생성자가 바인딩된다.
console.log(Champion.prototype);	// {constructor: ƒ}

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

한편, 프로토타입 역시 객체이므로 자신의 프로토타입에 대한 참조를 값으로 가지는 [[Prototype]] 내부 슬롯을 가진다. 프로토타입의 프로토타입은 Object.prototype이므로, Champion.prototype의 프로토타입(Champion.prototype.__proto__) 역시 Object.prototype이 된다.

객체 생성 후 프로토타입의 상속

모든 객체가 생성되기 전에 사용자 정의 생성자 함수 객체와 빌트인 생성자 함수 객체의 생성, 그리고 바인딩된 프로토타입의 생성이 완료된다. 생성된 프로토타입은 이후 생성자 함수나 리터럴 표기법으로 생성된 객체의 [[Prototype]] 내부 슬롯에 할당되어 상속된다.

15.4 객체의 프로토타입 결정

객체가 생성될 때 객체의 __proto__ 프로퍼티에 프로토타입 객체가 할당된다. 이때 할당되는 프로토타입은 객체를 생성하는 방식에 따라 달라진다.

  • 객체 리터럴
  • 생성자 함수
  • Object.create 메서드
  • 클래스(ES6)

세부적인 객체 생성 방식에 차이가 있으나 이들 모두 추상 연산 OrdinaryObjectCreate에 의해 생성된다.

추상 연산 OrdinaryObjectCreate

  • 추상 연산은 ECMAScript 사양에서 내부 동작의 구현 알고리즘을 표현하기 위해 사용한다.
  • 모든 객체는 생성될 때 추상 연산 OrdinaryObjectCreate가 호출된다.

OrdinaryObjectCreate의 동작

우선 객체의 생성 방식에 따라 프로토타입이 결정된다. 그다음 OrdinaryObjectCreate가 호출되어 다음과 같은 순서로 동작한다.

  1. 호출되며 다음과 같은 인자를 전달받는다.
    • (필수) 객체에 대하여 결정된 프로토타입
    • (옵션) 객체에 추가할 프로토타입 목록
  2. 빈 객체를 생성한다. 프로토타입 목록을 전달받았다면 해당 프로퍼티를 생성한 객체에 추가한다.
  3. 전달받은 프로토타입을 생성한 객체의 [[Prototype]] 내부 슬롯에 할당한다.
  4. 생성한 객체를 반환한다.

객체 리터럴로 생성한 객체의 프로토타입

다음은 리터럴 표기법으로 객체를 생성한 경우 OrdinaryObjectCreate에 전달되는 프로토타입이다.

리터럴 생성자 함수 프로토타입
객체 리터럴({}) Object Object.prototype
함수 리터럴(function () {}) Function Function.prototype
배열 리터럴([]) Array Array.prototype
정규 표현식 리터럴(//) RegExp RegExp.prototype

리터럴로 생성한 객체와 생성자 함수로 생성한 객체의 프로토타입이 같은 이유

  • 리터럴 표기법으로 생성한 객체와 생성자 함수로 생성한 객체는 세부 처리가 다를 수 있다.

  • 그러나 프로토타입과 생성자 함수는 더불어 생성되고 prototype 프로퍼티와 constructor 프로퍼티에 의해 연결되어있다. 리터럴로 생성한 객체도 상속을 위해 프로토타입이 필요하므로 가상의 생성자 함수를 가져야 한다.

  • 따라서 리터럴로 생성된 객체와 생성자 함수로 생성된 객체는 같은 프로토타입을 가진다고 볼 수 있다.

객체 리터럴과 Object 생성자 함수의 차이

  • Object 생성자 함수에 인수를 전달하지 않거나 nullish 값을 전달하면 내부적으로 추상 연산 OrdinaryObjectCreate를 호출하여 Object.prototype을 프로토타입으로 갖는 빈 객체를 생성한다.
  • 객체 리터럴의 평가는 OrderinaryObjectaCreate를 호출하여 빈 객체를 생성하고 프로퍼티를 추가하여 이루어진다.
  • OrdinaryObjectCreate를 호출한다는 점에서는 같지만 세부 처리가 다르므로 객체 리터럴로 생성한 객체는 Object 생성자 함수로 생성한 객체와 같지 않다.
({}).__proto__ === Object.prototype;	// true
Object.getPrototypeOf({}) === Object.prototype;	// true

객체 리터럴로 생성한 객체는constructor 프로퍼티나 hasOwnProperty 메서드 등을 가지지 않지만, Object.prototypeconstructor 프로퍼티나 hasOwnProperty 메서드 등을 상속받아 자신의 것처럼 사용할 수 있다.

함수 리터럴과 Function 생성자 함수의 차이

  • Function 생성자 함수로 생성한 함수는 함수 리터럴(함수 선언문, 함수 표현식)을 평가하여 생성한 함수와 달리 스코프와 클로저를 만들지 않는 등 세부 내용이 다르다.

생성자 함수로 생성한 객체의 프로토타입

생성자 함수로 객체를 생성하는 경우 OrdinaryObjectCreate에는 생성자 함수의 prototype 프로퍼티에 바인딩된 프로토타입이 전달된다.

Object.create로 생성한 객체의 프로토타입

Object.create(프로토타입, {
    프로퍼티키1: 프로퍼티 디스크립터1,
    프로퍼티키2: 프로퍼티 디스크립터2,
});

Object.create로 생성한 객체의 프로토타입은 첫번째 인수로 전달한 객체가 된다. 15.8 직접 상속 - Object.create를 참고한다.

그 밖의 객체의 프로토타입

Unless otherwise specified every built-in function and every built-in constructor has the Function prototype object, which is the initial value of the expression Function.prototype (20.2.3), as the value of its [[Prototype]] internal slot.

Unless otherwise specified every built-in prototype object has the Object prototype object, which is the initial value of the expression Object.prototype (20.1.3), as the value of its [[Prototype]] internal slot, except the Object prototype object itself. https://262.ecma-international.org/13.0/#sec-ecmascript-standard-built-in-objects

위와 같은 방법으로 생성하지 않은 객체, 즉 생성자 함수 객체와 이것에 바인딩된 프로토타입 객체의 프로토타입은 어떻게 결정되는가? 기본적으로 함수 객체의 프로토타입은 Function.prototype이므로, 생성자 함수 객체의 프로토타입 역시 Function.prototype이다. 이에 따라 Function 생성자는 다른 생성자와 조금 다르다.

  1. Function 생성자는 함수 객체를 생성하므로, Function.prototype은 함수다. 이와 달리, 다른 생성자는 일반 객체를 생성한다.
  2. Function 생성자는 함수 객체이므로, 다른 함수 객체와 마찬가지로 프로토타입으로 Function.prototype을 가진다. 이와 달리, 다른 생성자는 바인딩된 프로토타입을 자신의 프로토타입으로 가지지 않는다.
// Function 생성자의 프로토타입은 Function 생성자에 바인딩된 프로토타입이다.
Function.__proto__ == Function.prototype;	// true
/* Function 생성자와 달리, 다른 생성자 함수의 프로토타입은 해당 생성자에 바인딩된 프로토타입이 아니라 Function 생성자에 바인딩된 프로토타입이다.
*/
Object.__proto__ === Object.prototype;	// false
Object.__proto__ === Function.prototype;	// true

따라서 모든 객체의 프로토타입을 정리하면 다음과 같다.

  • Object.prototype의 프로토타입은 null이다.
  • Object.prototype을 제외한 모든 프로토타입의 프로토타입은 Object.prototype이다.
  • Function.prototype을 제외한 모든 함수의 프로토타입은 Function.prototype이다.
  • Function.prototype의 프로토타입은 Object.prototype이다.

따라서 생성자 함수의 프로토타입은 Function.prototype이다. 한편 생성자 함수에 바인딩된 프로토타입의 프로토타입은 아래와 같다.

  • Object.prototype을 제외한 생성자 함수에 바인딩된 프로토타입의 프로토타입은 Object.prototype이다.
  • Object.prototype의 프로토타입은 null이다.

15.5 프로토타입 체인

자바스크립트는 프로토타입 체인으로 객체지향 프로그래밍의 상속 개념을 구현한다. 프로토타입 체인(prototype chain)이란 프로퍼티를 검색하는 방법으로, 객체의 프로퍼티에 접근할 때 해당 객체가 프로퍼티를 가지고 있지 않다면 [[Prototype]] 내부 슬롯이 참조하는 객체를 따라올라가며 프로토타입의 프로퍼티를 순차적으로 검색하는 것이다.

프로토타입 체인의 종점

  • Object.prototype는 프로토타입 체인의 종점(end of prototype chain)으로서, 프로토타입 체인의 최상위에 위치한다.
  • Object.prototype에서도 프로퍼티를 찾지 못하는 경우 undefined를 반환한다.
  • Object.prototype[[Prototype]] 내부 슬롯의 값은 null이다.

프로토타입 체인의 동작

  1. 현재 객체에서 프로퍼티를 검색한다. 없다면 현재 객체의 [[Prototype]] 내부 슬롯이 바인딩하는 프로토타입 객체로 이동한다.
  2. 프로퍼티를 찾을 때까지 1번을 반복한다. 찾는다면 해당 프로퍼티를 반환하거나 메서드를 호출한다. 이때 메서드는 자신의 this에 최초의 탐색 객체를 바인딩한다.

왜 메서드는 프로토타입의 프로퍼티로 정의하는가?

생성자 함수에서 프로퍼티를 정의하는 것이 일반적인데 왜 메서드는 프로토타입에 정의하는 걸까?

/* 생성자 함수에서 메서드 정의하기 */
function Foo (name) {
    this.name = name;
    this.func = function () {
        console.log(this.name);
    };
}

const foo = new Foo('foo');
foo.func();	// foo

/* 프로토타입에 메서드 정의하기 */
function Bar (name) {
    this.name = name;
}
Bar.prototype.func = function () {
    console.log(this.name);
}
bar.func();	// bar

위 코드에서 foo.func()bar.func() 모두 객체의 name 프로퍼티를 잘 출력하고 있다. 그래서 후자는 전자의 syntax sugar처럼 보이지만 실제로는 그렇지 않다.

  1. 프로토타입 체인은 객체가 참조하는 프로토타입에서 프로퍼티를 검색하는 것이다. 즉 생성자 함수에서 정의한 프로퍼티는 프로토타입 체인에 존재하지 않는다.

    /* func는 생성자 함수에서 정의되었으므로 프로토타입 체인에 존재하지 않는다 */
    console.log(Foo.prototype.hasOwnProperty('func'));	// false
    /* func는 프로토타입에 정의되었으므로 프로토타입에 존재한다 */
    console.log(new Bar().hasOwnProperty('func'));	// false
  2. 생성자 함수에서 정의한 프로퍼티는 인스턴스마다 새로이 생성된다. 프로퍼티의 값이 객체라면, 동일한 내용이어도 각 인스턴스마다 새로운 객체가 생성되는 것이다.

    /* Foo의 인스턴스는 각자의 func을 가진다 */
    console.log(new Foo().hasOwnProperty('func'));	// true
    new Foo().func === new Foo().func;	// false
    /* Bar의 인스턴스는 func을 공유한다 */
    console.log(Bar.prototype.hasOwnProperty('func'));	// true
    new Bar().func === new Bar().func;	// true
  3. 이는 비효율적이므로 this의 특성(메서드로 호출하면 this의 값이 메서드를 호출한 객체가 된다)을 사용하여 메서드를 프로토타입의 프로퍼티로 정의하는 것이 적절하다.

생성자 함수에서 정의한 프로퍼티가 상속되지 않는 것은 Object.create로 확실히 알 수 있다.

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

const foo = new Foo();
const bar = Object.create(Foo.prototype);
console.log(foo.__proto__ === bar.__proto__);	// true
console.log(foo.name);	// foo
console.log(bar.name);	// undefined

foobar은 같은 프로토타입(Foo.prototype)을 가지지만 생성자 함수에 정의된 프로퍼티는 프로토타입 체인에 존재하지 않으므로, bar.nameundefined이다.

ES6의 클래스로 살펴보자.

class Foo {
    f1 = function() {}

    f2() {}
}
class Bar extends Foo {}

console.log(Foo.prototype.hasOwnProperty('f1'));	// false
console.log(Foo.prototype.hasOwnProperty('f2'));	// true

f1은 인스턴스마다 생성되는 프로퍼티이지만 f2는 프로토타입의 프로퍼티이다. 따라서 Foo의 인스턴스들은 f2는 같은 함수 객체를 참조하지만 f1은 각자 가지게 된다.

const b1 = new Bar();
const b2 = new Bar();
console.log(b1.f1 === b2.f1);	// false
console.log(b2.f2 === b2.f2);	// true

프로토타입 체인에 존재하지 않는 메서드 간접 호출하기

객체가 자신의 프로토타입 체인에 존재하지 않는 메서드를 호출하도록 하고 싶을 수 있다. 가령 인수의 합을 반환하는 함수 sum을 작성한다고 하자. sum은 정수의 배열 nums을 전달받는다.

function sum(nums) {
    return nums.reduce((acc, cur) => acc + cur, 0);
}
console.log(sum([1, 2, 3]));	// 6

numsArray의 인스턴스이다. 즉, nums의 내부 슬롯 [[Prototype]]Array.prototype을 바인딩한다. 따라서 Array.prototype.reduce를 사용할 수 있다. 그러나 유사 배열 객체 argumentsArray의 인스턴스가 아니므로 Array.prototype의 메서드를 사용할 수 없다.

function sum() {
    return arguments.reduce((acc, cur) => acc + cur, 0);
}
console.log(sum(1, 2, 3));	// TypeError: arguments.reduce is not a function

이때 Function.prototype.call이나 Function.prototype.applyArray.prototype.reduce를 간접 호출할 수 있다.

function sum() {
    return Array.prototype.reduce.call(arguments, (acc, cur) => acc + cur, 0);
}

console.log(sum(1, 2, 3));	// 6

Function.prototype.apply/call를 참고한다.

15.6 프로퍼티 쉐도잉

자바스크립트에서 객체지향 프로그래밍의 오버라이딩(overriding) 개념은 프로퍼티 쉐도잉이라고 한다. 오버라이딩이란 하위 클래스가 상위 클래스의 메서드를 재정의하여 사용하는 것이다. 프로퍼티 쉐도잉(property shadowing)은 계층 관계를 가진 두 객체가 동일한 이름의 프로퍼티를 가질 경우 하위 객체의 프로퍼티가 상위 객체의 프로퍼티를 가리는 것을 의미한다.

15.7 프로토타입 교체하기

프로토타입은 동적으로 교체할 수 있다. 두 가지 방법이 있다.

  • 생성자 함수의 prototype 프로퍼티로 모든 인스턴스의 프로토타입 교체하기
  • 인스턴스 객체의 __proto__ 접근자 프로퍼티로 특정 인스턴스의 프로토타입 교체하기

생성자 함수의 prototype 프로퍼티 사용하기

생성자 함수의 prototype 프로퍼티에 접근하여 프로토타입을 교체할 수 있다. 이 작업은 생성자의 모든 인스턴스 객체에 반영된다.

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

Champion.prototype = {
    sayHi() {
        console.log('안녕!');
    }
};

const champion = new Champion();

자바스크립트 엔진은 프로토타입을 생성할 때 constructor 프로퍼티에 생성자 함수를 바인딩한다. 이 경우 객체 리터럴에 constructor 프로퍼티를 명시하지 않았으므로 기존의 프로토타입 객체 Champion.prototypeconstructor 프로퍼티와 생성자 함수 Champion과의 바인딩이 파괴된다.

console.log(champion.constructor === Champion);	// false
console.log(champion.constructor === Object);	// true

따라서 프로토타입 체인에 의해 constructor 프로퍼티는 Object를 반환한다.

Champion.prototype.constructor === Champion;	// false
Champion.prototype.constructor === Object;	// true

생성자 함수의 prototype 프로퍼티를 직접 할당하면 생성자 함수와 기존의 프로토타입 객체의 constructor 프로퍼티 간의 바인딩이 파괴된다. 여기서는 Champion.prototypeChampion 간의 바인딩이 파괴되었다.

인스턴스 객체의 __proto__ 접근자 프로퍼티 사용하기

인스턴스 객체의 __proto__ 접근자 프로퍼티 혹은 Object.setPrototypeOf 메서드를 사용하여 프로토타입을 교체할 수 있다. 이 작업은 해당 인스턴스 객체에만 반영된다.

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

const champion = new Champion('lux');

const parent = {
    displayName() {
        console.log(`${this.name}`);
    }
}

Object.setPrototypeOf(champion, parent);

이 경우 parentconstructor 프로퍼티를 명시하지 않았으므로 인스턴스 객체 champion의 내부 슬롯 [[Prototype]]이 바인딩하는 객체의 constructor 프로퍼티는 생성자 함수 Champion를 바인딩하지 않는다.

console.log(champion.constructor === Champion);	// false
console.log(champion.constructor === Object);	// true

따라서 인스턴스 객체 chamionconstructor 프로퍼티에 접근하면 프로토타입 체인에 의해 constructor 프로퍼티는 Object를 반환하게 된다.

Champion.prototype.constructor === Champion;	// true
Champion.prototype.constructor === Object;	// false

인스턴스 객체의 __proto__ 접근자 프로퍼티로 직접 할당하면 인스턴스 객체의 내부 슬롯 [[Prototype]]이 가리키는 객체의 constructor 프로퍼티가 생성자 함수를 바인딩하지 않을 뿐, 기존의 프로토타입 객체의 constructor 프로퍼티와 생성자 함수의 바인딩이 끊어진 것은 아니다. 따라서 Champion.prototypeChampion 간의 바인딩이 파괴되지는 않았다.

15.8 직접 상속

객체에 명시적으로 프로토타입을 지정하여 상속받도록 할 수 있다. 세 가지 방법이 있다.

  • Object.create로 직접 상속하기
  • 객체 리터럴에서 __proto__로 직접 상속하기
  • ES6 클래스 사용하기

Object.create 사용하기

Object.create로 생성할 객체의 프로토타입을 지정할 수 있다.

Object.create(프로토타입, {
    프로퍼티키1: 프로퍼티 디스크립터1,
    프로퍼티키2: 프로퍼티 디스크립터2,
});

ESLint는 Object.create(null)로 프로토타입 체인의 종점을 생성할 수 있으므로 사용을 권장하지 않는다. nullish나 아무 값도 전달하지 않는다면 Object.prototype의 프로퍼티들을 사용할 수 없다. 이때 Object.prototype의 빌트인 메서드를 사용하려면 다음과 같이 간접적으로 호출한다.

const obj = Object.create(null);
obj.a = 1;
console.log(Object.prototype.hasOwnProperty.call(obj, 'a'));

객체 리터럴 사용하기

객체 리터럴 내부에 __proto__ 프로퍼티를 명시하여 객체의 프로토타입을 지정할 수 있다.

const obj = {
	__proto__: Object.prototype;
};

15.9 정적 프로퍼티와 프로토타입 프로퍼티의 구분

  • 정적(static) 프로퍼티와 메서드는 인스턴스를 생성하지 않아도 참조하고 호출할 수 있는 프로퍼티와 메서드이다.
  • 프로토타입 프로퍼티와 메서드는 생성한 인스턴스에서 참조하고 호출할 수 있는 프로퍼티와 메서드이다.

정적 프로퍼티와 메서드

function Champion() {}
/* 정적 메서드 생성 */
Champion.sayHello = function() {
    console.log('Hello world');
};
const champion = new Champion();
Champion.sayHello();	// Hello world
champion.sayHello();	// TypeError: champion.sayHello is not a function

champion의 프로토타입 체인에 sayHello가 존재하지 않기 때문에 오류가 발생한다.

프로토타입 프로퍼티와 메서드

프로토타입 메서드 내에서 인스턴스 혹은 프로퍼티를 참조하지 않는다면(this를 사용하지 않는다면) 정적 메서드로 변경할 수 있다.

function Champion() {}
/* 정적 메서드 생성 */
Champion.prototype.sayHello = function() {
    console.log('Hello world');
};
const champion = new Champion();
champion.sayHello();	// Hello world
Champion.sayHello();	// Champion.sayHello is not a function

프로토타입의 sayHello를 호출하려면 인스턴스를 생성해야 한다.

15.10 인스턴스의 타입 확인하기

instaceof로 객체가 어느 타입의 인스턴스인지 알 수 있다.

객체 instanceof 생성자함수;

객체의 프로토타입 체인에서 생성자함수prototype에 바인딩된 객체가 있다면 true, 그렇지 않다면 false로 평가한다.

15.11 프로퍼티 존재 확인하기

in 연산자

프로퍼티키 in 객체;

프로퍼티키객체의 프로토타입 체인 상에 존재한다면 true, 그렇지 않다면 false로 평가한다.

Reflect.has 메서드

Reflect.has(객체, 프로퍼티키);

프로퍼티키객체의 프로토타입 체인 상에 존재한다면 true, 그렇지 않다면 false로 평가한다. in 연산자와 동일하게 동작한다.

Object.prototype.hasOwnProperty 메서드

객체.hasOwnProperty(프로퍼티키);

객체가 고유하게 프로퍼티키를 가지면 true, 그렇지 않다면(상속받았다면) false로 평가한다.

15.12 프로퍼티 열거하기

상속받은 프로퍼티도 열거하기

for ... in

for (변수선언문 in 객체) {}

객체의 프로토타입 체인에 존재하는 프로토타입 객체의 모든 열거가능한 프로퍼티를 순회하며 열거한다. 열거가능한 프로퍼티는 내부 슬롯 [[Enumerable]]의 값이 true이다. Symbol형의 프로퍼티는 기본적으로 열거하지 않는다. 대부분의 모던 브라우저는 정수 프로퍼티에 대해서는 정렬하고 그 외의 프로퍼티는 정의된 순서로 나열하는 것을 보장한다.

배열의 경우 프로퍼티를 모두 열거하고 싶은 것이 아니라면 for문, for ... of문 또는 Array.prototype.forEach를 사용하는 것이 적절하다.

고유의 프로퍼티만 열거하기

Object.keys

Object.keys(객체);

객체의 열거가능한 프로퍼티를 배열로 반환한다.

Object.values

Object.values(객체);

객체의 열거가능한 프로퍼티의 값을 배열로 반환한다.

Object.entries

Object.entries(객체)

객체의 열거가능한 프로퍼티의 키와 값을 담은 배열들의 배열로 반환한다.

클래스 기반과 프로토타입 기반 언어의 차이

  • 클래스 기반 언어에서 클래스(class)는 추상적이며 객체는 클래스를 인스턴스화한 인스턴스(instance)이다.
  • 프로토타입 기반 언어는 프로토타입(prototypical object)를 가진다.
    • 새로운 객체가 초기 프로퍼티를 가지도록 템플릿으로서 사용된다.
    • 모든 객체는 다른 객체에 대한 프로토타입이 될 수 있다.
    • 모든 객체는 생성될 때 혹은 런타임에 프로퍼티를 생성할 수 있다.
클래스 기반 프로토타입 기반
클래스와 인스턴스 클래스와 인스턴스는 별개의 개체이다. 모든 객체는 다른 객체로부터 상속한다.
정의 클래스 정의로 클래스를 정의한다.
생성자 함수로 클래스를 인스턴스화한다.
생성자 함수로 일련의 객체들을 정의하고 생성한다.
새로운 객체 생성하기 new 연산자로 단일한 객체 생성 동일함.
객체 계층의 구성 존재하는 클래스(superclass)로부터 서브클래스(subclass)를 정의한 클래스 정의를 사용하여 객체 계층 구성 객체를 생성자 함수와 연관된 프로토타입으로 할당하여 객체 계층 구성
상속 모델 클래스 체인을 통한 프로퍼티 상속 프로토타입 체인을 통한 프로퍼티 상속
프로퍼티 동적 생성 클래스 정의는 클래스의 모든 인스턴스의 모든 프로퍼티를 정의.
런타임에 동적으로 프로퍼티 수정 불가능
생성자 함수 혹은 프로토타입이 일련의 초기 프로퍼티들을 명시
일련의 객체들과 개별적인 객체에 동적으로 프로퍼티 추가 혹은 삭제 가능

참고