Core js - 프로토타입

heyhey·2023년 11월 14일
0

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개의 댓글