프로토타입

프로토타입

JS는 기존 객체를 복사하여 새로운 객체를 사용하는 프로토타입 기반의 언어다.

프로토타입은 객체의 원형을 뜻하며 생성된 객체도 다른 객체의 원형이 될 수 있다.

프로토타입 기반 언어

JS는 객체를 상속하기 위해 프로토타입이라는 방식을 사용한다.

다른 객체로부터 메소드나 속성을 상속 받기 위해 프로토타입 객체를 가진다.

프로토타입 객체 역시 다른 프로토타입 객체를 상속 받을 수 있으며, 이를 프로토타입 체인이라 한다.

모든 객체는 [[Prototype]] 속성을 가지며 이는 null 이거나 다른 객체에 대한 참조가 된다.

객체에서 특정 속성이나 메소드를 찾을 때 객체에서 찾은 뒤 없는 경우 프로토타입에서 속성을 찾는다.

프로토타입에 대한 gettersetter__proto__ 또는 Object.getPrototypeOf / Object.setPrototypeOf를 이용한다.

__proto__는 기본적으로 브라우저에서만 지원하도록 규정되어있으나 대부분의 호스트 환경에서 지원한다.

프로토타입의 상속과 참조 순서

객체에서 메소드나 속성을 참조하면 우선적으로 객체 자체에서 찾은 뒤 없는 경우 프로토타입에서 찾는다.

상속 받은 프로토타입에서 가지고 있는 메소드 및 속성은 자식도 참조할 수 있다.

const animal = {
    name: '동물',
    walk() {
        console.log('걷기')
    }
}

const rabbit = {
    jumps: true,
    __proto__: animal
}

rabbit.jumps // "true" rabbit에 jumps가 있으므로 rabbit에서 참조
rabbit.name // "동물" rabbit에 없고, __proto__의 animal에 있으므로, animal에서 참조
rabbit.walk() // log"걷기" animal 에서 상속받아 호출 

주의사항

  • 프로토타입의 체인에서 순환 참조는 허용되지 않는다.

    Uncaught TypeError: Cyclic proto value

  • 프로토타입으로 객체나 null 이외의 자료형은 무시된다.

프로토타입에서의 this

프로토타입에서의 this는 호출 주체가 객체인지 프로토타입인지 상관 없이 . 앞의 객체가 대상이다.

const animal = {
    getThis() {
        return this
    }
}

const rabbit = {
    __proto__: animal
}

rabbit.getThis() // rabbit{}, 호출 주체가 rabbit이므로, this는 rabbit

for..in과 프로토타입

대부분의 키-값 관련 메소드는 상속 프로퍼티를 제외하지만 for..in은 상속 프로퍼티를 포함하여 순회한다.

const animal = {
    name: 'animal'
}

const rabbit = {
    jump: true,
    __proto__: animal
}

for(const v of Object.keys(rabbit)) console.log(v) // 자체 프로퍼티만 순회
/*
  "jump"
*/

console.log('---')

for(const k in rabbit) console.log(k) // 상속 프로퍼티까지 순회
/*
  "jump"
  "name"
*/

함수와 프로토타입

JS에서 함수를 정의하면 함수는 prototype을 가지고, 그 함수를 constructor로 가지는 함수의 프로토타입 객체가 생성된다.

new 키워드를 통해 객체를 생성하면 __proto__는 함수의 프로토타입 객체로 최초 한 번 참조한다.

/**
 * prototype: Person.prototype
 */
function Person(){}

/**
 * constructor: Person
 */
Person.prototype = {
    constructor: Person
}

/**
 * __proto__: Person.prototype
 */
const person = new Person()

prototype 속성

기본적으로 prototype 에는 constructor가 생기고, 이는 생성자로서 동작한다.

하지만 JS는 올바른 constructor를 보장하지 않기 때문에 사용에 주의가 필요하다.

따라서 프로토타입에 메서드나 속성을 추가하고 싶을 땐 prototype을 덮어쓰지 않고 원하는 값을 추가 또는 제거하는 형태가 좋다.

prototype을 덮어써 constructor가 사라진 경우

prototype을 실수로 덮어썼다면, 수동으로 다시 할당하면 다시 사용할 수 있다.

Person.prototype = {
    constructor: Person,
    // ...
}

주의사항

  • 프로토타입은 최초 객체 생성 시 할당되므로 생성 이후에 새로 할당된 프로토타입은 적용되지 않는다

    function Rabbit() {}
    
    Rabbit.prototype = { eats: true }
    
    const rabbit = new Rabbit()
    
    Rabbit.prototype = {}
    
    rabbit.eats // "true", rabbit이 생성되는 시점에서 {eats: true}가 할당되므로 이후에 새로 할당된 prototype은 반영되지않는다.
    // 단 이후 새로 생성하는 Rabbit 객체는 영향을 받는다.
    // 영향을 주고 싶다면 delete Rabbit.prototype.eats 형태로 사용해야한다.

네이티브의 프로토타입

일반적인 네이티브 객체

일반적으로 대부분의 객체는 프로토타입 체인에 Object.prototype을 가지고 있고, Object 의 프로토타입은 null 이다.

Array, Function 등의 다양한 네이티브 객체들은 대부분 Object.prototype를 프로토타입으로 가지고 있다.

프로토타입 내부에서 중복되는 메서드나 속성이 있다면 체인에서 더 가까운 값을 참조한다.

원시값의 프로토타입

원시값은 객체가 아니기 때문에 프로퍼티에 접근하려 하면 임시 래퍼 객체를 통해 처리가 된다.

Number, String, Boolean 등의 래퍼 객체는 각자의 prototype을 가지고 있으며, 이들은 Object.prototype를 프로토타입으로 갖는다.

네이티브 프로토타입 변경

일반적으로 네이티브 프로토타입의 변경은 권장되지 않는다.

네이티브 프로토타입의 변경이 필요한 경우는 폴리필을 만드는 경우가 있다.

// Array.prototype.map의 폴리필
if (!Array.prototype.map) {

  Array.prototype.map = function(callback, thisArg) {

    var T, A, k;

    if (this == null) {
      throw new TypeError(' this is null or not defined');
    }

    var O = Object(this);
    var len = O.length >>> 0;
      
    if (typeof callback !== 'function') {
      throw new TypeError(callback + ' is not a function');
    }
      
    if (arguments.length > 1) {
      T = thisArg;
    }
      
    A = new Array(len);
    k = 0;
      
    while (k < len) {
      var kValue, mappedValue;
        
      if (k in O) {
        kValue = O[k];
        mappedValue = callback.call(T, kValue, k, O);
        A[k] = mappedValue;
      }
      k++;
    }
    return A;
  };
}

프로토타입으로 메서드 빌려오기

프로토타입에 있는 메서드도 빌려와서 사용할 수 있다.

예를 들어 유사배열에서 join이 필요한 경우 아래처럼 활용할 수 있다.

Array.prototype.join.call({0: 'a', 1: 'b', length: 2}) // "a,b"
유사 배열은 배열처럼 보이지만 키 값이 숫자이고 length를 가지는 객체입니다.

단순한 객체

프로토타입이 null인 경우 __proto__를 갖지 않는 단순한 객체를 만들 수 있다.

일반 객체에 __proto__에 문자열을 할당하면 적용되지 않는다면, 단순한 객체에서는 제대로 적용된다.

이는 프로토타입을 가지지 않으므로 toString 등의 메소드를 사용할 수 없지만 완전한 연관 배열의 역할을 하는 객체를 만든다.

const plainObj = Object.create(null)
const normalObj = Object.create(Object.prototype) // {}

plainObj['__proto__'] = 'plain'
normalObj['__proto__'] = 'normal'

console.log(plainObj['__proto__']) // "plain"
console.log(normalObj['__proto__']) // Object.prototype{}, __proto__에는 null이나 객체만 할당 가능함

Written by@[esllo]
plain developer

GitHubTwitterLinkedIn