본문 바로가기
언어/JavaScript

(JavaScript)프로토타입

by 흥부와놀자 2021. 8. 29.

왜 이걸 공부하게 됬는가?

리액트, Angular, vue 등 많은 것들이 자바스크립트 라이브러리이다. 앞으로 프론트엔드를 공부하는데에 있어서 자바스크립트의 중요성은 정말 크다고 생각했다. 프로토타입은 자바스크립트에서 객체를 생성하는 중요 방식이므로 앞으로 언젠가는 꼭 짚고 가야했다. 

 

프로토타입 패턴에 대해서

프로토타입 패턴은 객체를 생성할때 원본객체를 복사해서 생성하는 방식을 말한다. 이러한 프로토타입 패턴은 perl, lua등 여러 언어에서 사용되고 있고 자바스크립트에서도 중요하게 사용된다. 물론 JS(자바스크립트)에서 사용되는 방식이 단순히 객체를 복사해서 생성한다는 것보단 복잡한데, 결국 근간은 비슷하다고 생각한다.

 

JS에선 Class가 아닌 함수로 객체를 생성한다.

자바에선 객체를 생성하기에 앞서 클래스를 만들고 new를 통해 해당 클래스의 인스턴스를 생성한다. 하지만 JS에선 클래스가 아닌 함수를 정의하고 new를 통해 객체를 생성하게 된다. 

function A(){}
var a = new A()

물론 var A = {prop:1} 과같은 리터럴 방식도 있지만 이런 방식 역시 내부는 함수를 통해 생성한다.

 

뭘 복제해서 생성한다는걸까?

new를 통해 생성된 객체는 생성자 함수의 prototype객체를 복제하여 만들어진다. 모든 함수에는 처음 만들어질때 해당 함수의 prototype객체도 같이 만들어진다. 그리고 이 prototype객체를 복사하여 객체가 생성된다. 

모든 함수에는 prototype 프로퍼티가 있고, 이 프로퍼티는  prototype객체를 참조한다. 

https://gocoding.tistory.com/242

위의 그림과 같은 구조라고 보면된다. 

 

constructor

함수의 prototype객체를 살펴보면 constructor프로퍼티가 존재한다. 의미그대로 생성자를 뜻하는데 해당 prototype객체를 생산한 함수객체를 참조한다. 이러한 프로퍼티는 왜 존재하는 걸까? 생성된 객체는 자신이 어떤 함수에서 생성된건지 알수 있어야 한다. 해당 객체는 함수의 Prototype객체를 복제한것이므로 참조하게 되면 생성자 함수를 알수 있을것이다. 그렇다면 객체는 어떻게 자신을 복제한 원본(Prototype객체)에 접근할 수 있을까?  

 

__proto__

모든 객체는 어떠한 prototype객체의 복사본이라고 볼수 있는데, 이렇게 자신의 복사본에 접근하기 위해 __proto__프로퍼티를 참조한다.

위의 생성된 a객체가 자신을 생성한 함수 A를 알기위해선 A의 prototype객체의 constructor를 참조해야 한다. 그때 a는 __proto__로 prototype객체에 접근하고 constructor를 참조한다.

function A(){}
var a = new A()
console.log(a.__proto__.constructor)
console.log(a.__proto__.constructor === A)

실행결과

__proto__처럼 자신을 복제한 객체(원본)에 접근하는 프로퍼티를 프로토타입 링크라고 한다. 브라우저 콘솔창에선 __proto__가 아닌 다르게 나올수 있는데, 이는 브라우저 마다 다르다. 자주 쓰는 크롬의 경우엔 [[Prototype]]이 이 프로토타입링크라고 보면된다.

 

프로토타입 체인

자바스크립트 내의 모든 객체들(String, Boolean 등)은 프로토타입 방식으로 복제되어 만들어진다. 이러한 모든 객체들은 Object함수의 Prototype객체를 복제하여 만들어진다. 그리고 이 Object의 Prototype객체는 마지막이기에 __proto__ 프로퍼티의 참조가 존재하지 않는다. 

정말 모든 prototype객체들의 최종 프로토타입 객체는 Object함수의 prototype객체인지 확인해 보자.

위의 a객체의 __proto__를 쭉 찾아볼것이다.

function A(){}
var a = new A()
var a1 = a.__proto__.__proto__
var a2 = a.__proto__.__proto__.__proto__
console.log(a1)
console.log(a2)

실행결과

a의 복제 객체인 a1은 Object함수의 Prototype객체이다. 해당 객체안에 여러 함수들이 보이는데, 해당 객체가 복제된 모든 자바스크립트의 객체들이 저러한 함수들을 상속받는다. 또한 이러한 객체의 프로토타입링크를 한번더 확인하면 null이 나오는것을 볼 수 있다.

이렇게 프로토타입으로 이루어진 객체들의 관계를 프로토타입 체인이라고 한다.

 

this와 프로토타입 방식의 객체 프로퍼티 할당 차이

객체의 메서드나 프로퍼티를 설정하는 방법에는 총 2가지가 있다. 

1.  this 사용

function A()
{
    this.func = function(){
	}
}
var a1 = new A();
var a2 = new A();
console.log(a1.func===a2.func)

함수 객체안에 this를 사용하여 프로퍼티와 메서드를 정의할 수 있다. 이때 함수내부의 this는 새롭게 생긴 객체를 의미하기 때문에 a1과 a2의 func함수는 서로 다른 메모리 공간에 존재한다. 그렇기에 위의 결과는 false가 나온다.

 

2. 프로토타입 사용

function A()
{

}
A.prototype.func = function(){}
var a1 = new A();
var a2 = new A();
console.log(a1.func===a2.func)

 이번엔 생성자 함수의 prototype 프로퍼티를 통해 메서드를 정의해보았다. 이렇게 정의된 A함수의 Prototype객체는 복제되어 a1과 a2가 되고, 둘다 원본객체를 참조하게 되기에 위의 결과는 true가 나온다.

 

function A()
{
    this.func = function(){console.log("this 사용")}
}
 A.prototype.func = function(){console.log("prototype 사용")}
var a1 = new A();
a1.func()

그렇다면 이러한 경우엔 결과가 어떻게 출력될까?

바로 "this 사용"으로 출력되는걸 알 수 있다. 왜 원본객체의 프로퍼티가 아닌 this를 통해 새로 생긴 객체의 프로퍼티를 먼저 참조하게 되는걸까?

 

프로토타입 룩업

객체에서 프로토타입을 찾는 프로세스는 이러하다.

1. this키워드로 생성한 A.func()가 존재하는지 확인한다.

2. 만약 존재하지 않다면 __proto__를 통해 원본객체로 올라간다.

3. 찾지 못한다면 원본객체가 null이 되거나(최종 도착), 해당 프로퍼티를 찾을 때까지 __proto__를 통해 올라간다. 

4. 찾은 프로퍼티를 반환한다. 없으면 undefined를 반환한다.

 

이렇기 떄문에 모든 객체들은 자신의 프로토타입 체인 내에 있는 객체들의 모든 프로퍼티와 메소드에 접근할 수 있다. 또한 이러한 방식을 활용해서 객체들을 상속시킬수 있다.

https://evan-moon.github.io/2019/10/27/inheritance-with-prototype/

 

프로토타입을 이용한 상속

Object.create 메소드 사용(Internet Explorer 9부터 지원된다.)

//1
function Super(_prop)
{
    this.prop1 = _prop
}
Super.prototype.func1 = function(){console.log(this.prop1)}
//2
function Sub(_prop)
{
Super.call(this,_prop)
}
//3
Sub.prototype = Object.create(Super.prototype)
//4
Sub.prototype.constructor = Sub;
var sub = new Sub("func1")
//5
console.log(sub)
console.log(sub.__proto__)
console.log(sub.__proto__.__proto__)
console.log(sub.__proto__.__proto__.__proto__)
console.log(sub.__proto__.__proto__.__proto__.__proto__)
sub.func1()

1. 먼저 부모 함수인 Super를 정의해주고 생성자로 받은 내부 프로퍼티와 원본객체의 함수 func1을 정의해준다.

2. 자식 함수인 Sub를 정의해준다. Super.call(this,_prop)은 자식 객체를 생성할때 부모 함수의 생성자를 부르기 위해 사용했다.

3. Object.create(객체)를 해주면 해당 객체의 속성을 갖는 새로운 객체를 만들어준다. 부모의 원본에서 새롭게 만들어진 객체는 자식의 원본으로 들어감으로써 __proto__로 접근 가능한 새로운 프로토타입 체인이 생긴다. 이것이 자바스크립트의 상속이다.

4. 상속된 객체의 생성자가 부모가 아닌 자식 함수가 되어야 하므로 자식 원본객체의 생성자에 자식 함수를 넣어준다.

5. 결과

5. 실행결과

Sub객체 / Sub의 원본 객체 / Super의 원본객체 / Object의 원본객체 / 마지막 null이 나온것을 볼수 있다.

 

프로토타입을 공부해본 의의

사실 ES6로 들어오며 class문법이 생기고 상속 역시 우리에게 익숙한 문법으로 나와있다. 하지만 그럼에도 불구하고 이렇게 레거시 문법을 공부했을때 얻은 의의들이 있는데,

 

첫번째는 자바스크립트의 객체 생성과정을 알게 되었단 것이다. ES6에 class문법이 생겼다해도 그 내부는 여전히 프로토타입으로 동작한다고 한다. 이런점에서 그동안 겉핧기로만 배웠던 자바스크립트에 대해 한발자국 더 다가갔다.

 

두번째는 우리가 브라우저에서 객체를 출력했을때 나오던 constructor, [[prototype]]와 같은 프로퍼티들의 의미를 알게 됬단것이다.

몰랐던 궁금증이 풀렸기에 기분이 좋았다.