TIL10, JS: 프로토타입으로 상속 구현하기*

sunghoonKim·2020년 11월 28일
4

자바 스크립트는 프로토타입 기반 객체 지향 언어라고 한다. 프로토타입?이라는 것을 이용하여 상속을 구현한다고 하는데, (클래스의 개념이 도입된 ES6 전에는 말이다.) 이 프로토 타입이라는 개념이 다른 언어에서는 듣지도 보지도 못한 것이라 이해하는데 꼬박 하루가 걸렸다. 그렇게 하루동안 공부한 내용을 정리한다. 생활코딩오승환님 블로그가 많은 도움이 되었다.


프로토타입?

자바스크립트에 모든 함수는 생성될 때 prototype 속성이 부여되고, 모든 객체는 생성될 때 __proto__ 속성이 부여된다. 이 부분은 생소하고 잘 와닿지 않겠지만, 자바스크립트가 이렇게 설계된 것이므로 그냥 받아드리는 수 밖에 없다.

자바스크립트에서는 객체 생성자 함수를 생성할 때, 단순히 함수가 생성되는 게 아니라, 2가지 일이 일어난다.

  1. 각 함수에 해당하는 프로토타입 객체가 생성된다. 이 객체에는 constructor 속성이 자동으로 부여되고, 이 constructor 속성은 생성된 생성자 함수를 가리킨다.
  2. 함수에 prototype 이라는 속성이 부여되고, 이 속성은 프로토타입 객체를 가리킨다.

이미지 출처: 오승환님 블로그

생성된 함수와 프로토타입 객체는 위와 같이 서로를 상호 참조한다.


프로토타입의 용도

이러한 프로토타입 객체의 속성 값을 이용하여, 인스턴스에서 공통적으로 사용할 부분들을 정의할 수 있다.

이미지 출처: 오승환님 블로그

인스턴스들은 __proto__ 속성을 통해 생성자 함수의 프로토타입 객체와 연결되는데, 이는 인스턴스들이 모두 부모 생성자의 프로토타입 객체와 연결되어 있기 때문에, 프로토타입 객체에 부여한 속성 값들에 접근할 수 있다는 것을 의미한다. 예를 들어,

function Person(name, first, second, third){
    this.name=name;
    this.first=first;
    this.second=second;   
}
// 프로토타입 객체에 sum 메소드를 저장.
Person.prototype.sum = function(){
    return 'prototype : '+(this.first+this.second);
}

var lee = new Person('lee', 10, 10);
console.log("lee.sum()", lee.sum()); // 'lee.sum()' 'prototype : 20'

lee 객체에는 sum 메소드가 없었지만, __proto__ 속성의 링크를 타고 프로토타입 객체를 참조하여, 그 안에 있는sum 메소드를 실행했다.

var kim = new Person('kim', 10, 20);
kim.sum = function(){
    return 'this : '+(this.first+this.second);
}
console.log("kim.sum()", kim.sum()); // 'kim.sum()' 'this : 30'

다만 kim.sum() 이 호출되었을때, kim 객체 안에 sum 메소드가 있다면, 프로토타입 객체까지 갈 필요없이 kim 객체에서 sum 메소드가 실행된다.


__proto__ 를 이용한 객체간 상속 구현

위에서 __proto__ 속성은 각각의 인스턴스를 부모 프로토타입 객체와 연결을 해주어, 부모 프로토타입 객체 속의 속성을 사용할 수 있도록 해주었다. 만약 그러한 연결을 객체끼리 해준다면, 하나의 객체에서 다른 객체의 속성에 자유롭게 접근할 수 있게 될것이다. 마치 객체끼리 서로를 상속하는 듯이 말이다.

1. __proto__ 를 이용한 객체 상속

var superObj = {superVal:'super'}
var subObj = {subVal:'sub'}
subObj.__proto__ = superObj;

console.log('subObj.superVal =>', subObj.superVal); // 'subObj.superVal =>' 'super'

위에서 보듯, subObjsuperVal 가 없지만, 서로 간의 상속을 통해 superObjsuperVal 을 문제 없이 접근할 수 있다.

혼동하면 안되는 것이,subObj.superVal = 'sub' 을 할 경우, 서로가 상속하고 있다고 해서 superObjsuperVal 을 수정하는 것이 아니라는 것이다. 이 표현식의 의미는 subObj새로이 superVal 속성을 만들라는 것이다.

// 코드는 위에서 이어진다.
subObj.superVal = 'sub';
console.log('superObj.superVal =>', superObj.superVal); // 'superObj.superVal =>' 'super'

만약 subObj 를 통하여 superObj 를 변경하고 싶다면, subObj.__proto__.superVal = 'sub' 로, __proto__ 를 타고 superObj 로 접근하여야 한다.

2. Object.create()

__proto__ 가 자바스크립트에 구현은 되있지만, 이는 비표준으로 취급된다. (비표준으로 취급할꺼면 구현을 왜 해두고 사용하는지는 이해가 안가지만 말이다.) 따라서, 표준으로 취급되는 동등한 코드를 사용하도록 하자. Object.create() 가 그것인데, 괄호 안에 들어가는 객체를 상속함을 의미한다.

var superObj = {superVal:'super'}
var subObj = Object.create(superObj);
subObj.subVal = 'sub';

console.log(subObj); // { subVal: 'sub', __proto__: { superVal: 'super' } }
console.log(subObj.__proto__ === superObj); // true

함수의 여러 용법

자바스크립트는에서 함수는 혼자 있으면 개인이고, new가 앞에 있으면 객체를 만드는 신이고, call을 뒤에 붙이면 용병이고, bind를 붙이면 분신술을 부리는 놀라운 존재입니다. 자바스크립트의 함수의 놀라움을 느껴보세요. -생활코딩

1. call()

함수를 call() 을 통해서 호출하는 것으로 this 의 컨텍스트를 자유자재로 바꿀수 있다.

var kim = {name:'kim', first:10, second:20}
var lee = {name:'lee', first:10, second:10}
function sum(prefix){
    return prefix+(this.first+this.second);
}
// sum();
console.log("sum.call(kim)", sum.call(kim, '=> ')); // 'sum.call(kim)' '=> 30'
console.log("lee.call(kim)", sum.call(lee, ': ')); // 'lee.call(kim)' ': 20'

위와 같이, sum.call()kim 을 인자로 넘겼을 시, this 의 컨텍스는 kim 이고, lee 를 넘겼을 시, 컨텍스트는 lee 이다.

첫번째 인자가 this 의 컨텍스트를 결정하고, 이후로 주어지는 인자는 패러미터이다.

2. bind()

함수를 bind() 를 통해서 호출하면, 주어진 인자로 this 의 컨텍스트가 결정된 새로운 함수를 리턴한다.

var kim = {name:'kim', first:10, second:20}
var lee = {name:'lee', first:10, second:10}
function sum(prefix){
    return prefix+(this.first+this.second);
}
// sum();
var kimSum = sum.bind(kim, '-> ');
console.log('kimSum()', kimSum()); // 'kimSum()' '-> 30'

call 과는 달리, 새로운 함수가 생성 되었고, 따라서 kimSum() 으로, 새로운 함수를 호출하였다.


프로토타입을 이용한 클래스 상속 구현

ES6에 도입된 클래스 문법를 이용해서 구현하는 것과 비교하려면 여기를 참조

1. 클래스 선언

function Person(name, first, second){
    this.name = name;
    this.first = first;
    this.second = second;
}

Person.prototype.sum = function(){
    return this.first + this.second;
}

constructor 부분은 동일하게 this 키워드를 이용하여 구현하고, 클래스 메소드는 파생된 인스턴스에서 접근가능 하도록 프로토타입 객체에 속성을 부여해 구현한다.

2. 생성자 상속

function PersonPlus(name, first, second, third){
    Person.call(this, name,first,second); // 여기서 this 는 PersonPlus를 의미
    this.third = third;
}

PersonPlus.prototype.avg = function(){
    return (this.first+this.second+this.third)/3;
}

상속되는 서브 클래스의 생성자에서는, call 키워드를 통해 Personthis 의 컨텍스트를 PersonPlus 로 변환한뒤, 나머지 name, fisrt, second 인자를 넘겨주는 것으로 슈퍼 클래스의 생성자를 상속 한다.

3. 프로토타입으로 수퍼 클래스의 메소드와 서브 클래스의 메소드 연결

// 첫번째 방법
PersonPlus.prototype.__proto__ = Person.prototype;


// 두번째 방법
PersonPlus.prototype = Object.create(Person.prototype);
PersonPlus.prototype.constructor = PersonPlus;
PersonPlus.prototype.avg = function(){
    return (this.first+this.second+this.third)/3;
}

두가지 접근법이 있다. 첫번째로, PersonPlus 의 프로토타입 객체에 있는 __proto__ 속성을 Person 의 프로토타입 객체에 연결시켜, PersonPlus 객체가 Person 객체 속에 있는 sum 을 호출할 수 있도록 한다. 제일 깔끔한 방법이지만, 비표준이라는 단점이 있다. (모든 브라우저가 __proto__ 를 지원하지는 않는다.)

두번째로, 좀더 표준 표현인 Object.create() 를 이용하는 것. 이렇게 되면 Person 의 프로토타입 객체를 기반으로 하는 새로운 객체를 만들어 PersonPlus 의 프로토타입 객체를 교체한다. 이 접근법은 2가지 문제점이 일으킨다.

  1. Person 의 프로토타입 객체로 교체가 되어버렸으므로, PersonPlus 의 프로토타입 객체의 constructorPerson 을 가리킨다. 따라서, 이를 다시 PersonPlus 로 가리키도록 재설정해주어야 한다.
  2. 재설정하는 과정에서, 기존 PersonPlus 프로토타입 객체는 아무곳에도 연결되지 못한 채 버려졌기 떄문에, 그 안에 만들어준 avg 메소드를 호출할 수 있는 방법이 없어진다. 따라서 PersonPlus 의 프로토타입 객체에 다시 만들어주어야 한다.

4. 클래스 상속 구현 전체 코드

function Person(name, first, second){
    this.name = name;
    this.first = first;
    this.second = second;
}
Person.prototype.sum = function(){
    return this.first + this.second;
}
 
function PersonPlus(name, first, second, third){
    Person.call(this, name,first,second);
    this.third = third;
}
 
// PersonPlus.prototype.__proto__ = Person.prototype;
PersonPlus.prototype = Object.create(Person.prototype);
PersonPlus.prototype.constructor = PersonPlus;
PersonPlus.prototype.avg = function(){
    return (this.first+this.second+this.third)/3;
}
 
var kim = new PersonPlus('kim', 10, 20, 30);
console.log("kim.sum()", kim.sum()); // 'kim.sum()' 30
console.log("kim.avg()", kim.avg()); // 'kim.avg()' 20
console.log('kim.constructor', kim.constructor); // 'kim.constructor' ƒ PersonPlus()

프로토타입이 무엇인지 그리고 프로토타입을 이용해서 상속을 어떻게 구현하는지에 대해 알아보았다.

PersonPlus.prototype = Object.create(Person.prototype); 
PersonPlus.prototype.constructor = PersonPlus;

위와 같이 때려맞추는 식의 코드를 내제하는 것처럼 문제의 요소가 꽤 있다. 프로토타입이 무엇인지 공부하는 목적으로 알고만 있고, 실제 구현은 클래스 문법을 통해서 하도록 하자. ES6를 지원 안하는 브라우저가 있다면 걱정말자. 갓 바벨이 알아서 잘 때려맞춰줄 것이니 👍

1개의 댓글

comment-user-thumbnail
2020년 11월 29일

js공부하면서 prototype에 대해 항상 궁금했는데 감사합니다 !!

답글 달기