Core js - 프로토타입

heyhey·2023년 11월 14일

JS는 프로토타입 기반 언어 입니다. 클래스 기반 언어에서는 '상속'을 사용하지만
프로토타입 언어는 어떤 객체를 원형(prototype)으로 삼고 이를 복제(참조) 함으로써 상속과 비슷한 효과를 얻습니다.

1. 프로토타입의 개념 이해

1-2 Constructor, prototype, instance

프로토타입은 이 그림만 이해하면 된다고 합니다.
이 그림은 다음 코드를 추상화한 것입니다.
var instance = new Constructor()

  • 어떤 생성자 함수(Constructor)를 new 연산자와 함께 호출합니다.
  • Constructor 에서 정의된 내용을 바탕으로 새로운 인스턴스가 생성됩니다.
  • 이 때 instance에는 __proto__라는 프로퍼티가 자동으로 부여됩니다.
  • 이 프로퍼티는 Constructor의 prototype이라는 프로퍼티를 참조합니다.

'prototype' 은 객체입니다. 이름 참조하는 __proto__ 또한 역시 객체입니다.

prototype 객체 내부에는 인스턴스가 사용할 메서드를 저장합니다.
그러면 인스턴스에서도 숨겨진 프로퍼티인 __proto__ 에 접근이 가능하게 됩니다.

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

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

이제 Person의 인스턴스는 __proto__ 프로퍼티를 통해 getName을 호출할 수 있습니다.

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

instance의 __proto__ 가 Constructor의 prototype 프로퍼티를 참조하므로 결국 둘은 같은 객체를 바라봅니다.
메서드 호출이 undefined를 반환했지만, 에러 없이 메서드를 호출이 가능했다 라는 점이 중요합니다.
this._name 을 반환하기 때문에 this를 찾아보면 suzi.__proto__ 라는 것을 알수 있습니다. this의 binding이 잘못되었다는 것인데, __proto__ 없이 인스턴스에서 곧바로 메서드를 사용하면 됩니다.

suzi.getName() // Suzi

__proto__ 를 빼게 되면 this는 instance가 되는 것이 맞지만, 이게 왜 동작하는지 의문입니다. 정답은 __proto__생략이 가능하기 때문입니다. 그리고 이 정의를 바탕으로 Js 구조가 구성됐다고 봐도 됩니다.__proto__ 를 생략하지 않으면 this는 suzi.__proto__ 를 가리키지만 생략하면 suzi를 가리킵니다.

다시 그림을 보고 아래의 글을 따라 그려봅시다.

new 연산자로 Constructor 를 호출하면 인스턴스가 만들어집니다.
이 인스턴스의 생략가능한 __proto__ 는 Constructor의 prototype을 참조합니다.

  • JS는 함수에 자동으로 객체인 prototype 프로퍼티를 생성해 놓는데,
  • 해당 함수를 생성자 함수로서 사용할 경우(new),
  • 그로부터 생성된 인스턴스는 숨겨진 프로퍼티인 __proto__ 가 자동적으로 생성되며,
  • 이 프로퍼티는 생성자 함수의 prototype 프로퍼티를 참조합니다.
  • __proto__ 는 생략 가능하도록 구현되어 있습니다.

생성자 함수의 Prototype에 어떤 메서드나 프로퍼티가 있다면 인스턴스에서도 마치 자신의 것처럼 해당 메서드나 프로퍼티에 접근할 수 있습니다.

개발자 도구로 확인할 때 짙은색 옅은 색의 차이는 enumerable속성의 차이입니다. 짙은색이 열거가 가능한 형태입니다.

예시를 통해 알아보겠습니다.

var arr = [1,2]
console.dir(arr)
console.dir(Array)

arr 의 내용을 보면,

0:1
1:2
length:2
__proto__ : Array(0)

이렇게 나오게 됩니다.
Array일 경우는

arguments: {...}
caller:{...}
from : f from()
isArray : f isArray()
length : 1
name : "Array"
prototype : Array(0)

arr 의 __proto__와 Array의 prototype 이 같은 내용인 것을 확인할 수 있습니다.

  • Array 를 New 연산자와 함께 호출해서 인스턴스를 생성하든, 그냥 리터럴 ([])을 생성하든 인스턴스가 만들어집니다.
  • 이 인스턴스의 __proto__는 Array.prototype 을 참조하는데, __proto__ 가 생략 가능하도록 설계되어 있어 __proto__ 의 메서드를 마치 자신의 것처럼 호출할 수 있습니다.
  • 한편 Array 의 prototype 프로퍼티 내부에 있지 않은 from, isArray 같은 경우에는 인스턴스가 직접 호출할 수는 없습니다. 이들은 Array 생성자 함수에서 직접 접근해야 합니다.
    Array.isArray(arr)

1-2 constructor 프로퍼티

생성자 함수의 프로퍼티인 Prototype 객체 내부에는 consturctor 이라는 프로퍼티가 존재합니다. __proto__ 객체 내부에도 마찬가지 입니다.
이 프로퍼티는 원래의 생성자 함수 (자기 자신)를 참조합니다.
이 프로퍼티는 원형이 무엇인지 알 수 있는 수단이 됩니다.

var arr = [1,2]
Array.prototype.constructor === Array
arr.__prototype__.constructor === Array
arr.constructor === Array 

var arr2 = new arr.constructor(3,4) // [3,4]

생성자 함수의 prototype 프로퍼티를 참조하여 constructor에 바로 접근이 가능합니다.

constructor 접근 방법

var Person = function(name){
	this.name = name
}
var p1 = new Person('hi')
var p1Proto = Object.getPrototypeOf(p1)
var p2 = new Person.prototype.constructor('hi2')
var p3 = new p1Proto.constructor('hi3')
var p4 = new p1._proto_.constructor('hi4')
var p5 = new p1.constructor('hi5')

2 프로토타입 체인

2-1. 메서드 오버라이드

만약 인스턴스가 동일한 이름의 프로퍼티 또는 메서드를 가지면 어떻게 될까요?

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

var iu = new Person('hi')
iu.getName = function(){
	return 'helllllooo'
}
iu.getName() // 'helllllooo'

중복으로 할당을 하게 되었을 때, 이 결과 값은 iu 객체에 있는 getName 이 호출됩니다.
이 현상을 메서드 호버라이드라고 합니다.
메서드를 찾는 방법은 가장 가까운 대상인 자신의 프로퍼티를 검색을 하게 되고, 없으면 그 다음으로 가까운 대상인 __proto__를 검색하게 됩니다. 중복 할당 시 교체가 아니라 위에 얹인다 라는 표현이 조금 더 어울리겠습니다.

iu.__proto__.getName() // undefined

이건 __proto__ 의 name이 없기 때문일 것입니다.
call이나 bind 로 this의 값을 할당해주면 문제가 해결됩니다.

iu.__proto__.getName.call(iu)

2-2 프로토타입 체인

객체의 내부 구조를 살펴봅니다. __proto__의 내부를 확인해보면 또 __proto__가 있습니다. 이 안에는 또 __proto__가 있습니다. __proto__ 또한 객체이기 때문입니다. 그렇기 때문에 Object.prototype 내부의 메서드도 자신의 것처럼 실행할 수 있습니다.

__proto__ 프로퍼티 내부에서 __proto__ 프로터티가 연쇄적으로 이어진 형태를 Prototype 체인이라고 합니다. 이 체인을 따라가며 검색하는 것을 프로토타입 체이닝이라고 합니다.

어떤 메서드를 호출합니다. JS 엔진은 데이터 자신의 프로퍼티를 검색해서 메서드가 있다면 실행하고, 없으면 __proto__ 를 검색해서 실행하고.. 반복합니다.

var arr = [1,2]
Array.prototype.toString.call(arr); // 1,2
Object.prototype.toString.call(arr); // [object Array]
arr.toString() ; //1,2 Array 의 Proto를 처리

arr.toString = function(){
	return this.join('_') 
}
arr.toString() // 1_2

arr 는 배열이기 때문에 arr.__proto__Array.prototype을 참조하고 Array.prototype는 객체이므로 Array.prototype.__proto__는 Object.prototype 을 참조합니다.

toString 메서드는 Array에도 있고, Object에도 있습니다. 하지만 결과는 Array.prototype.toString 의 결과값이 나옵니다.

이게 재귀적으로 들어가면 메모리를 할당할까요? 다행히 무한으로 반복되어 들어가더라도, 결국 같은 프로퍼티를 가리키므로 메모리가 낭비되진 않습니다.

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

어떤 생성자 함수이든 prototype 은 반드시 객체이기 때문에 Object.prototype 이 언제나 프로토타입 체인의 최상단에 존재하게 됩니다. 따라서 객체에서만 사용할 메서드는 프로토타입 객체 안에 정의할 수 없습니다. 이걸로 인해 다른 데이터 타입도 해당 메서드를 사용하게 되기 때문입니다.

이럴 경우는 static 메서드를 통해 부여합니다.

2-4 다중 프로토타입 체인

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,90) 

Grade 를 통해 유사 배열을 만들어보았습니다.

생성자 함수를 직접 만든 김에 인스턴스에서 배열 메서드를 직접 쓸 수 있게 하고 싶습니다.
그래서 Prototype을 배열의 인스턴스를 바라보게 합니다.

Grade.prototype = []

위와 같이 적게 되면 서로 별개되어 있던 데이터가 연결되어 하나의 프로토타입 체인 형태를 띄게 됩니다.

g.push()
g.pop()

g 인스턴스의 입장에서는 프로토타입 체인에 따라,

  • g 객체 자신이 지니는 멤버
  • Grade의 prototype
  • Array.prototype
  • Object.prototype
    까지 접근이 가능합니다.
profile
주경야독

0개의 댓글