웹프로그래밍/JavaScript

[JavaScript] 프로토타입(Prototypes)의 이해

정민교 2022. 2. 25. 16:26

개 념

MDN의 발췌한 내용은 다음과 같다.

모든 객체들이 메소드와 속성들을 상속 받기 위한 템플릿으로써 프로토타입 객체를 가진다.

그냥 보기에는 설명이 복잡하고 애매하다. 그래서 예제와 그림등을 사용하여 조금 더 자세히 알아보도록 하겠다.

먼저 JS에는 Class라는 개념이 없다. 때문에 기존 객체를 복사해서 새로운 객체를 생성한다. 이걸 프로토타입 기반 언어라고 하는데, 이렇게 생성된 새로운 객체도 다른 객체의 원형이 될 수 있다. 이걸 프로토타입 체인(prototype chain)이라 부르며 객체지향적인 프로그래밍을 할 수 있게 해준다.

JS에서는 객체의 prototype(객체 멤버인 __proto__ 속성으로 접근 가능한)과 생성자의 prototype 속성의 차이를 알아야하는데, 전자는 '개별 객체의 속성이'며 후자는 '생성자의 속성'이다. 이 말인 즉 Object.getPrototypeOf(new Apple())의 return 값이 Apple.prototype와 동일한 객체라는 의미이다. 전자와 후자의 차이점을 정확하게 이해하려면 JS의 함수의 구조와 객체 내부의 구조부터 정확하게 알고 있어야한다.

 

함수의 내부구조와 객체의 내부구조

function Person(){} // Person의 prototype 속성은 프로토타입 객체를 참조

// 생성된 모든 객체는 Person prototype 객체를 참조
var Min = new Person(); 
var Gyo = new Person();

① 속성이 아무것도 없는 Person이라는 함수가 정의 되고 파싱단계에서 Person함수의 prototype 속성은 Person Prototype 객체를 참조한다. ② 반대로 Person prototype 객체의 멤버인 constructor 속성은 Person 함수를 참조하는 구조이다. ③ 여기서 Person Prototype 객체는 new라는 연산자와 Person함수를 통해서 생성된 모든 객체의 원형이 되는 객체이다.(생성된 모든 객체가 참조한다.)

JS는 기본 데이터 타입 boolean, number, string 그리고 null, undefined을 제외하고는 모두 객체이다. (사용자정의 함수, new 연산자로 생성된 인스턴스도 객체 등) 객체 안에는 기본적으로 proto 속성이 있는데, 이 속성은 객체가 만들어지기 위해 사용된 원형 prototype 객체(그림에서 Person Prototype 객체)를 참조하는 역할을 한다.

 

프로토타입 객체

[예제1]

function Person(){} // Person의 prototype 속성은 프로토타입 객체를 참조

// 생성된 모든 객체는 Person prototype 객체를 참조
var Min = new Person(); 
var Gyo = new Person();

// 프로토타입 객체에서 동적으로 멤버(getType) 추가
Person.prototype.getType = function() {
    return "휴먼";
}

// 원형을 복사로 생성된 객체는 추가된 멤버(getType) 사용가능
console.log(Min.getType()); // 휴먼
console.log(Gyo.getType()); // 휴먼

Person 함수를 정의 했을 때 다른 곳에 생성되는 Person Prototype 객체는 prototype 객체 자신이 다른 객체의 원형이 되는 객체이다. 모든 객체는 Person prototype 객체에 접근할 수 있고, prototype 객체도 동적으로 런타임에 멤버를 추가할 수 있다. Person prototype을 원형으로 해서 복사된(생성된) 모든 객체는 추가된 멤버를 사용할 수 있다.

다만, prototype 객체에 멤버를 추가,수정,삭제할 때는 함수안의 prototype 속성을 사용해야하고, prototype 멤버를 읽을 때는 함수 안의 prototype 속성 또는 객체 이름으로 접근한다.

 

[예제 2]

function Person(){} // Person의 prototype 속성은 프로토타입 객체를 참조

// 생성된 모든 객체는 Person prototype 객체를 참조
var Min = new Person(); 
var Gyo = new Person();

// 프로토타입 객체에서 동적으로 멤버(getType) 추가
Person.prototype.getType = function() {
    return "휴먼";
}

Min.getType = function() {
    return "사람";
}

console.log(Min.getType());  // 사람
console.log(Gyo.getType());  // 휴먼

Gyo.age = 27;

console.log(Min.age);  // undefined
console.log(Gyo.age);  // 27

Person prototype 객체를 원형으로 하는 Min 객체에서 getType 멤버를 수정을 하였는데 이렇게 되면 원형인 prototype 객체에 있는 멤버를 수정하는 것이 아니라 자신의 객체에 멤버를 추가한 것이다. 결국 Min 객체의 getType은 prototype의 getType 멤버를 호출한 것이 아닌 Min 객체 자신에 추가된 getType멤버를 가져오는 것이다. Person prototype에 있는 getType을 수정하고 싶은 경우에는 prototype 속성을 사용해서 수정해야한다.(아래 코드 참조)

Person.prototype.getType = function () {
    return "사람";
}

console.log(Gyo.getType()); // 사람

 

프로토타입

JS에서는 기본 데이터 타입을 제외한 모든 것이 객체라고 위에서 언급했다. 객체가 만들어지려면 자신을 만드는데 사용된 원형인 prototype 객체를 이용해서 객체를 만든다. 이때 만들어진 객체 안에 __proto__ 라는 속성(멤버)이 자신을 만들어낸 원형인 prototype 객체를 참조하는 숨겨진 링크가 있다.숨겨진 링크를 프로토타입이라고 한다. 크롬 개발자도구로 디버깅을 하게되면 아래와 같이 언젠가 한번 객체에 __proto__ 라는 속성이 있는 것을 확인했을 것이다. 

정리하자면, JS에서의 프로토타입Person 함수의 멤버인 prototype 속성이 Person prototype 객체를 참조하는 속성이고 'new 함수명()' 으로 생성된 객체의 prototype 객체를 지정해주는 역할을 하는 프로토타입이며 생성된 객체의 __proto__ 속성자신을 만들어낸 원형인 Person prototype 객체를 참조하는 숨겨진 링크인데 이 링크로 Person prototype 객체 멤버에 접근하는 용도로서의 프로토타입이다.

 

Prototype을 활용한 코드 재사용

Classical 방식

Java에서 인스턴스를 생성하는 방법과 유사(new 연산자를 사용해 인스턴스를 생성)

 

[예제 1] 기본방법

// 부모 함수
function Person(name) {
    this.name = name;   // name = "민교";   
}

// Person 프로토타입 객체에 getName 속성 추가
Person.prototype.getName = function () {
    return this.name;
}

// 자식 함수
function Man(name){}

// 자식 함수의 프로토타입 객체의 속성을 부모 프로토타입 객체의 속성으로 변경
Man.prototype = new Person();

var man1 = new Man();
var man2 = new Man("정민교");

console.log(man1.getName());  // 민교
console.log(man2.getName());  // 민교

먼저 코드를 보면 Person함수의 Prototype 객체는 getName() 이라는 멤버를 추가했고 Man 함수의 Prototype 객체는 아무런 멤버도 선언하지 않았다. ③ 여기서 Man함수의 Prototype 객체를 Person 함수로 생성된 객체로 바꾸게 되면, ④ Man함수는 Man Prototype 객체와 Person Prototype 객체 모두 참조하게 된다. 그렇기 때문에 자식 인스턴스를 생성할 때 인자를 넘기더라도 부모 인스턴스를 생성할 때의 인자를 넘겨주지 못해 man2 인스턴스 생성자 인자로 "정민교"를 넣어도 부모 생성자에 인자를 넘겨주지 않았으므로 default 값인 "민교"가 출력된다. 물론 인스턴스를 생성할 때마다 부모의 함수를 호출할 수도 있지만 매우 비효율적이다.

 

[예제 2] 생성자 빌려 쓰기

function Person(name) {
    this.name = name; 		// name = "민교"
}

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

function Man(name) {
    Person.apply(this, arguments);
}

var man1 = new Man("정민교");
console.log(man1.name); // 정민교

Man 함수를 정의하는 부분을 보면 아까와는 달리 Man 함수 내부에서 Apply함수를 사용해 부모 객체인 Person 함수의 this를 Man 함수안의 this로 바인딩한다. (new 연산자로 Man함수의 "정민교"로 파라미터를 줄때 Person함수의 this.name에 바인딩 해 Man의 파라미터를 arguments의 유사배열로 받아서 Person함수를 실행한다.) 이렇게 될 경우 부모 객체의 멤버를 복사해서 자신의 것으로 만들어 버리기 때문에 부모 객체의 this로 된 멤버들만 물려받게 되는 단점이 있다. 그래서 결국에는 Person Prototype 객체의 멤버들을 물려받지 못한다. 기본방법에서와는 달리 man1객체에 Person Prototype객체의 링크가 없는걸 확인하면 된다.

 

[예제 3] 생성자 빌려 쓰고 프로토타입 지정

function Person(name) {
    this.name = name; 		// name = "민교"
}

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

function Man(name) {
    Person.apply(this, arguments);
}
Man.prototype = new Person();

var man1 = new Man("정민교");
console.log(man1.getName()); // 정민교

이전 코드와 달라진 점이 있다면 Man함수 하단에 Man함수의 prototype 객체를 Person함수로 생성된 객체로 지정하는데 이번에는 Person 객체 속성(this.name)에 대해 참조를 가지는 것이 아닌 복사본을 통해서 자신의 것으로 만듬과 동시에 기본방법과 동일하게 Person prototype 객체에 대한 링크도 참조가 된다. 때문에 Person의 Prototype 객체 멤버도 사용할 수가 있고 위의 방법들과의 차이점이 있다면, man1 인스턴스에 name 멤버를 가지고 있다는 점이다. 그러나 이 방법은 부모 생성자를 2번 호출한다는 단점이 있다. 때문에 name에 대해서 man1 객체와 Person함수를 이용한 객체(new Person())에도 name이 있는 것을 볼 수 있다.

 

[예제 4] 프로토타입 공유

function Person(name) {
    this.name = name; 		// name = "민교"
}

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

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

var man1 = new Man("정민교");
console.log(man1.getName()); // 정민교

이 방법은 ④ Man함수의 Prototype 속성을 Person함수의 Prototype 속성이 참조하는 객체로 설정했다. Man 함수를 통해 생성된 인스턴스는 Person함수를 통해 생성된 객체(new Person())을 거치지 않고 Person 함수의 Prototype 객체를 부모로 지정해서 인스턴스를 생성한다. 이 때 Person함수의 내용은 상속받지 못하기 때문에 상속받으려는 부분은 Person Prototype 객체에 작성해야 원하는 결과를 얻을 수가 있다.

 

Prototypal 방식

인스턴스를 생성함과 동시에 Prototype 객체를 지정

[예제 1] 

var person = {
    type : "휴먼",
    getType : function() {
        return this.type;
    },
    getName : function() {
        return this.name;
    }
};

var jung = Object.create(person);
jung.name = "민교";

console.log(jung.getType());  // 휴먼
console.log(jung.getName());  // 민교

이 방법은 Object.create()를 사용해 인스턴스를 생성과 동시에 Prototype 객체를 지정하는데, 이 Object.create() 함수는 첫 번째 파라미터로 부모 객체로 사용할 객체를 넘겨주고, 두 번째 파라미터는 선택 파라미터로 자식 객체의 속성에 추가되는 부분을 넣어준다. person 객체는 객체 리터럴 방식으로 객체를 선언하고 자식 객체인 jung은 Object.create()를 이용해 첫 번째 파라미터로 person을 넘겨받아 객체를 생성했다. 단 한줄로 객체를 생성함과 동시에 부모객체인 person객체의 속성도 물려받았다. 위의 classical 방식보다 훨씬 간결하면서 다양한 상황을 생각할 필요도 없어서 해당 방식을 많이 선호한다.


참고 포스팅

https://developer.mozilla.org/ko/docs/Learn/JavaScript/Objects/Object_prototypes

https://www.nextree.co.kr/p7323/