프로토타입

Ordinary·2023년 6월 5일
0

프로토타입의 개념 이해

기본적으로 자바스크립트는 프로토타입 기반 언어입니다. 클래스 기반 언어에서 상속을 사용하는 것처럼 프로토타입 기반 언어에서는 어떤 객체를 원형으로 삼아서 이를 참조함으로써 상속과 비슷한 효과를 얻습니다.

자바스크립트에서는 생성자 함수를 new연산자와 함께 호출하면 함수에 정의된 내용을 바탕으로 인스턴스라는 새로운 객체를 반환합니다.

인스턴스에는 __proto__라는 프로퍼티가 자동으로 부여되는데, 생성자 함수의 prototype이라는 속성을 참조하고 있습니다. prototype은 인스턴스가 사용할 프로퍼티와 메서드를 저장하고 있는 객체입니다.

var Person = function (name) {
	this._name = name;
}

Person.prototype.getName = function() {
	return this._name;
}

var suzi = new Person('suzi');
suzi.__proto__.getName() //undefined

suzi라는 인스턴스가 생성되면 __proto__가 자동으로 생성되어 Personprototype객체를 바라보게 됩니다. getName이라는 함수를 prototype 객체에 등록했기 때문에 suzi 에서도 __proto__를 통해서 사용할 수 있습니다.

여기서 undefined가 나온 이유는 getNamethissuzi.__proto__이기 때문입니다. __proto__생략 가능한 프로퍼티이기 때문에 이를 생략하면 제대로 thissuzi 인스턴스로 바인딩됩니다.

suzi.getName() //suzi; -> suzi(.__proto__).getName();

생성자 함수를 통해 객체 생성할 때, 일어나는 일을 다시 순서대로 나열해보면 다음과 같습니다.

  • 함수가 생성될 때, 자동으로 prototype 프로퍼티가 생성된다.
  • 함수가 new연산자를 이용해서 인스턴스를 생성할 때, 인스턴스는 __proto__라는 프로퍼티가 자동으로 생성된다.
  • __proto__는 생성자 함수의 prototype 프로퍼티를 참조한다.

때문에, 생성자 함수의 prototype에 어떤 프로퍼티나 메서드가 등록되어 있다면, 생성되는 모든 인스턴스에서는 마치 자신의 것처럼 메서드에 접근할 수 있습니다.

Constructor

생성자 prototype 객체 내부에는 자기 자신을 참조하는 constructor라는 프로퍼티가 존재합니다. constructor 프로퍼티는 인스턴스의 __proto__ 안에도 존재하기 때문에 인스턴스의 원형이 무엇인지 알 수 있습니다.

let arr = [1, 2];
Array.prototype.constructor == Array // true
arr.__proto__.constructor == Array // true
arr.constructor == Array // true

위 예시를 범용적으로 표현해보면 다음과 같이 정리할 수 있습니다 다음 5개의 경우는 모두 동일한 대상을 가리킵니다.

[Constructor] //생성자 함수
[instance].__proto__.constructor // prototype의 constructor참조 = 생성자 함수
[instance].constructor //__proto__생략된 형태
Object.getPrototypeOf([instance]).constructor
[Constructor].prototype.constructor

프로토타입 체인

메서드 오버라이드

오버라이드라는 것은 원본이 그대로 있는 상태에서 다른 대상을 그 위에 얹는 개념입니다. 메서드 오버라이드라는 것은 메서드 위에 메서드를 덮어씌웠다는 의미입니다.

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

Person.prototype.getName = function() {
	return this.name;
}

let iu = new Person("지금");

iu.getName = function(0 {
	return `바로 ${this.name}`;
}

iu.getName(); //바로 지금

인스턴스가 동일한 이름의 프로퍼티나 메소드를 가지게 되면 메서드 오버라이드가 일어납니다.

메서드를 호출할 때는 가장 가까운 대상인 자신의 프로퍼티와 메소드를 먼저 검색하고 없으면 그 다음 대상인 __proto__를 검색하는 순서로 진행하기 때문에 위 예시처럼 인스턴스 안에서 동일한 이름의 메서드가 정의된 경우, 해당 함수를 우선으로 호출합니다.

만약, 원본에 접근하고 싶으면 다음 방법을 사용할 수 있습니다.

iu.__proto__.getName.call(iu);

이처럼 인스턴스에 동일한 이름의 프로퍼티 또는 메서드를 가지고 있는 상황일 때, 인스턴스는 __proto__ 이 아닌 자신의 것을 먼저 호출합니다.

프로토타입 체인

자바스크립트는 메서드나 프로퍼티에 접근하려고 할 때, 현재 객체에 없다면 객체의 프로토타입에서 찾습니다. 프로토타입에도 없다면 프로토타입의 프로토타입을 찾는 방식으로 거슬러 올라가게 됩니다.

이렇게 어떤 데이터의 __proto__ 프로퍼티 내부에서 다시 __proto__ 프로퍼티가 연쇄적으로 이어진 것을 프로토타입 체인이라고 하고, 이 체인을 따라가며 검색하는 것을 프로토타입 체이닝이라고 합니다.

객체 전용 메서드의 예외사항

어떤 생성자 함수이든 prototype은 객체이기 때문에 프로토타입 체인의 최상단에는 Object.prototype이 존재합니다. 이 때문에, 일반적으로 어떠한 데이터 타입이라도 프로토타입 체인을 따라서 Object.prototype에 접근할 수 있습니다. 이는 장점이 될 수도 있지만, 때로는 문제를 일으킵니다.

Object.prototype.getEntries = function() {
  let res = [];
  for (let prop in this) {
    if (this.hasOwnProperty(prop)) {
      res.push([prop, this[prop]]);
    }
  }
  return res;
};

let obj = {a: 1, b: 2, c: 3};
console.log(obj.getEntries()); // [["a",1], ["b",2],["c",3]]

let str = 'abc';
console.log(str.getEntries()); // [["0","a"], ["1","b"], ["2","c"]]

위 예시에 따르면, 우리는 객체에서만 사용할 의도로 getEntries라는 함수를 prototype객체에 등록했습니다. 따라서 다른 데이터 타입이 getEntries를 호출하면 오류가 발생하게끔 돼야 할 텐데 의도와는 다르게 정상 작동하는 것을 볼 수 있습니다.

즉, Object.prototype을 다른 데이터 타입에서도 참조하고 있기 때문에 객체에서만 사용할 목적으로 메서드를 등록해도 다른 데이터 타입도 사용할 수 있습니다.

    참조!

    위 예시에서의 str은 String으로 primitive타입입니다.
    primitive 타입은 객체가 아니므로 프로퍼티나 메서드를 가질 수 없습니다. 
    하지만, 위 예시에서는 객체와 유사하게 동작하는 것을 확인할 수 있습니다.

    자바스크립트에서는 primitive 타입으로 프로퍼티나 메서드를 호출하게 되면, primitive 타입과
    연관된 객체로 변환되어 일시적으로 프로토타입 객체를 공유하게 됩니다.
    즉, 내장 객체의 Global Objects(ex)String, Number, Array, ...) 중 하나인 String 객체로
    변환되어 String.prototype에 접근할 수 있게 되었고, 최종적으로 Object.prototype을
    참조하게 되어 앞서 정의한 getEntries()를 호출할 수 있었던 것입니다.

    [[참고 아티클]](https://poiemaweb.com/js-prototype) 

따라서, 객체 전용 메서드를 구현하고 싶으면 prototype이 아닌 Object의 static메서드로 등록할 수밖에 없습니다. 또한, 인스턴스를 this로 사용하기 위해 대상 인스턴스를 인자로 직접 주입해줘야 합니다. 즉, "메서드명 앞에 있는 대상이 곧 this"라는 방식을 포기하고 메서드를 호출할 때마다 대상 객체를 인자로 직접 주입해야 합니다.

해당 예시로, Object.freeze()가 있습니다.

let obj = { name: "John Doe" };
Object.freeze(obj);

obj.name = "Jan Da";  // 실패하거나 오류 발생
console.log(obj.name);  // "John Doe"

반대로 Object.prototype에는 어떤 데이터에서도 활용할 수 있는 범용적인 메서드들을 정의합니다.

ex) toString, hasOwnProperty, valueOf, isPrototypeOf, …

객체에만 동작하는 메서드를 정의하고 싶을 때는 모든 데이터형이 참조 가능한 __proto__를 통해서 정의하는 것이 아니라 Object에 스태틱 메서드 형태로 정의할 수 밖에 없다.

반대로, 어떤 데이터 타입에서도 범용적으로 활용할 수 있는 메서드가 Object.prototype 객체에 정의되어 있다.

다중 프로토타입 체인

자바스크립트의 기본 내장 데이터 타입들은 모두 프로토타입 체인이 1단계이거나 2단계로 끝나는 경우가 많지만 사용자가 새롭게 만드는 경우 그 이상도 얼마든지 가능합니다.

__proto__를 연결하는 방법은 __proto__가 가리키는 대상 즉, 생성자 함수의 prototype이 다른 상위 생성자 함수의 인스턴스를 바라보면 됩니다.

var Grade = function () {
	var args = Array.prototype.slice.call(arguments);
	for(var i = 0; i < args.length; i++) {
		this[i] = args[i];
	}
	this.length = args.length;
}

var g = new Grade(100, 80);

Grade.prototype = []; //프로토타입 체인 연결!

Grade의 경우, 인자로 받은 arguments들을 내부에서 배열의 형태로 저장하고 length 프로퍼티를 가지는 유사배열객체로써, 배열 메서드는 사용할 수 없습니다.

Grade의 인스턴스가 배열의 prototype 메서드를 직접 사용하기 위해서 생성자 함수 Grade의 prototype이 배열 인스턴스([])를 바라보도록 했습니다.

g.__proto__ 즉, 인스턴스의 __proto__객체는 Grade의 prototype인 배열의 인스턴스를, 배열 인스턴스의 prototype은 Array의 prototype을 바라보는 다중체이닝을 통해서 g에서도 배열의 메서드를 사용할 수 있게 되었습니다.

0개의 댓글