Pandaman Blog

[JS] Javascript의 Prototype (프로토타입) 본문

Front end/Javascript

[JS] Javascript의 Prototype (프로토타입)

oyg0420 2020. 1. 21. 21:56

Javascript Prototype(프로토타입)

JavaScript는 흔히 프로토타입 기반 언어(prototype-based language)라 불립니다.— 모든 객체들이 메서드와 속성들을 상속받기 위한 템플릿으로써 프로토타입 객체(prototype object)를 가진다는 의미입니다. 프로토타입 객체도 또다시 상위 프로토타입 객체로부터 메서드와 속성을 상속받을 수도 있고 그 상위 프로토타입 객체도 마찬가지입니다. 이를 프로토타입 체인(prototype chain)이라 부르며 다른 객체에 정의된 메서드와 속성을 한 객체에서 사용할 수 있도록 하는 근간입니다. -MDN-

MDN Javascript prototype을 검색하면 위와 같이 프로토타입에 대해 설명합니다. 음.. 그래도 명확하게 이해가 되지 않아 영어사전에서 다시 prototype을 검색해봅니다. 한국어로 원형이라는 결과를 얻을 수 있습니다. 아하! 원형! 위에서 설명하는 프로토타입이라는 것이 객체의 원형을 말하는구나~!라고 알 수 있었습니다.

프로토타입 속성

그렇다면 prototype 속성이 정확히 무엇인지 확인해보겠습니다.

function Pandaman (age, name) {
  this.age = age;
  this.name = name;
}

console.log(Pandaman.prototype);
// {contructor: f}

Pandaman이라는 함수를 생성했습니다. 크롬 개발자 도구를 사용해 Pandaman.prototype을 직접 확인해보겠습니다. prototype속성에는 Pandaman함수의 원형을 생성자로 갖고 있을 것을 확인할 수 있습니다. 이를 통해서 생성자 함수로 동작합니다.
또한 prototype 속성에는 상속이 되는 속성과 메서드가 정의되어 있습니다.

프로토타입 링크(prototype link)

__proto__는 무엇일까요?
MDN문서를 확인해보면 __proto__은 객체가 초기화될 때 프로토타입으로 사용된 객체를 가리킨다고 설명하고 있습니다.
예제를 통해 확인해보겠습니다.

Pandaman.prototype.job = 'developer';
const pandaman1 = new Pandaman(2, 'pandaman1');

console.log(pandaman1.__proto__);
console.log(Pandaman.prototype);

위 예제에서 Pandaman이라는 함수를 생성했고 Pandaman()를 통해 pandaman1이라는 인스턴스를 생성했습니다.

pandaman1.__proto__를 확인해보면 pandaman1.__proto__Pandaman.prototype의 속성을 참조했다는 것을 확인할 수 있습니다. 그래서 pandaman1job속성에 접근이 가능한 이유입니다.
이처럼 객체가 생성될때 정의된 프로토타입을 참조하는 객체를 프로토타입 링크(__proto__) 라고 합니다. 따라서 모든 객체는 프로토타입 링크(__proto__) 속성이 존재합니다. 따라서 이 링크가 참조한 프로토타입을 타고 올라가며 속성과 메소드를 탐색합니다.

arrow function은 왜 상속을 못할까요?

다음은 화살표 함수(arrow function)로 함수를 생성해보겠습니다.

const fn = () => {
 console.log('arrow function test');
}

fnprototype를 확인해보면 undefined를 출력합니다. fn함수를 생성할 때 prototype속성이 존재하지 않습니다. 따라서 상속이 되는 속성과 메서드를 담은 prototype속성이 존재하지 않기 때문에 상속이 가능하지 않습니다. 물론 new연산자를 사용하면Uncaught TypeError: fn is not a constructor에러를 출력합니다.

프로토타입 체인(prototype chain)

프로토타입 객체도 상위 프로토타입 객체로부터 메서드와 속성을 상속받을 수도 있고 그 상위 프로토타입 객체도 마찬가지입니다. 이를 프로토타입 체인(prototype chain)이라 부르며 다른 객체에 정의된 메서드와 속성을 한 객체에서 사용할 수 있도록 하는 근간입니다. - MDN -

사실 위에서 설명한 예제에서도 프로토타입 체인을 확인할 수 있습니다. Pandaman의 속성인 name, age는 pandaman1에서도 접근이 가능합니다. 그리고 toString, valueOf 메서드에도 접근이 가능한데요. 그 이유는 Pandaman()의 프로토타입 속성의 프로토타입 링크(__proto__)를 통해 Object.prototype에 접근이 가능하기 때문입니다.

실제로 Object에 정의되어 있는 메서드를 pandaman1에서 호출한다면 어떻게 될지 확인해보겠습니다.

pandaman1.toString();

브라우저는 우선 pandaman1 객체가 toString() 메서드를 갖고 있는지 확인합니다.
없다면 pandaman1의 프로토타입 링크(__proto__)에 toString() 메서드가 있는지 확인합니다.
여전히 없기 때문에 Pandaman()prototype속성의 프로토타입 링크(__proto__)가 toString()메서드를 갖고 있는지 확인합니다. 있다면 호출하여 끝납니다.

다음은 리터럴로 객체 생성을 했을 때 프로토타입 체인이 어떻게 일어나는지 확인해보겠습니다.

const person = { name: 'oyg0420', age: 31 }

person이라는 객체를 생성했습니다. person의 프로토타입 링크(__proto__)는 Object객체를 나타냅니다.
위의 리터럴로 객체를 생성하는 방법은 const person = new Object({ name: 'oyg0420', age: 31 });와 동일한 걸 보시면 이해하기 쉬우시겠죠? 참고로 Objectprototype속성의 프로토타입 링크(__proto__)는 null 이 됩니다.

함수를 이용해서 객체를 생성했을 때 프로토타입 체인이 어떻게 일어나는지 확인해보겠습니다.

// 1)
function Graph() {
  this.vertexes = [];
  this.edges = []'
};

// 2)  
Graph.prototype = { addVertex: function(v) {  
this.vertexes.push(v);  
}};

// 3)  
var g = new Graph();

// 4)  
g.addVertex(1);  
g.vertexes; // [1]

1) Graph라는 함수를 생성했습니다.
2) Graph의 prototype속성에 addVertex함수를 추가했습니다.
3) new를 g를 생성했습니다. g의 __proto__가 Graph의 prototype 속성과 동일한지 확인합니다.
4) 직접 g에 addVertex를 추가하지 않았지만 g의 프로토타입 링크(__proto__)인 Graph의 prototype속성에는 addVertex가 추가되어 있으므로 g에서는 이제 addVertex에 접근이 가능해졌습니다.

다음은 Object.create() 이용하여 객체를 생성했을 때 프로토타입 체인이 어떻게 일어나는지 확인해보겠습니다.
Object.create라는 메서드를 호출하여 새로운 객체를 만들 수 있습니다. 상속할 객체를 첫 번째 인수로 지정됩니다.
첫 번째 인수로 들어온 데이터가 생성된 객체의 프로토타입 링크(__proto__)가 됩니다. 한번 확인해보겠습니다.

// 1)
const person = { name: 'oyg0420', age: 31, job: 'developer' };
// 2)
const personA = Object.create(person);
console.log(personA.__proto__); // {name: "oyg0420", age: 31, job: "developer"}
// personA -> person -> Object.prototype -> null
// 3)
const personB = Object.create(personA);
console.log(personB.__proto__); // {}
// personB -> personA -> person -> Object.prototype -> null

1) person객체를 생성했습니다. person의 프로토타입 링크(__proto__)는 Object.prototype을 참조하는 것을 알 수 있습니다.
2) Object의 create 메서드를 통해서 person객체를 첫 번째 인수에 넣고 personA객체를 생성했습니다. 생성된 personA객체의 프로토타입 링크는 person이 되는 것을 알 수 있습니다.
3) Object의 create 메서드를 통해서 personA객체 첫 번째 인수에 넣고 personB객체를 생성했습니다. 생성된 personB객체의 프로토타입 링크는 personA이 되는 것을 알 수 있습니다.

this와 Prototype을 이용한 프로퍼티/메소드 추가 방식의 차이점

다음은 Prototype, this의 속성과 메서드 추가 방식의 차이점에 대해 알아보겠습니다.

아래는 this를 이용해 속성과 메서드 추가 방식의 예제입니다..

function Pandaman(age, name, job) {
  this.age = age;
  this.name = name;
  this.job = job;
  this.fn = function () {
    alert('i am using this');
  }
}

const pandaman1 = new Pandaman(30, 'pandaman1', 'developer');
const pandaman2 = new Pandaman(20, 'pandaman2', 'doctor');

Pandaman생성자 함수를 이용해 pandaman1pandaman2 두 개의 인스턴스 객체를 생성했습니다. pandaman1pandaman2의 각각 다른 객체입니다. 총 6개의 속성과 2개의 메서드가 생성되었습니다. 이처럼 생성자 함수 내부의 this를 이용해 속성과 메서드를 정의하고 인스턴스 객체를 생성한다면 생성된 데이터(속성/메서드)가 메모리에 저장될 것입니다.

아래는 Prototype를 이용해 속성과 메서드 추가 방식의 예제입니다.

function Pandaman() {}
Pandaman.prototype.name = 'pandaman1';
Pandaman.prototype.age = 30;
Pandaman.prototype.job = 'developer';
Pandaman.prototype.fn = function () {
    alert('i am using prototype');
}

const pandaman1 = new Pandaman();
const pandaman2 = new Pandaman();

pandaman1pandaman2속성을 확인해보면 프로토타입 링크를 제외하고 다른 속성을 확인할 수 없습니다. pandaman1pandaman2name, age, job, fn()가 자신의 속성/메서드는 아니지만, 프로토타입 링크가 Pandaman.prototype속성을 참조하기 때문에, Pandaman.prototype의 속성인 name, age, job, fn()에 접근할 수 있습니다. 어떤 방법이 더 좋다고 말하기는 어렵지만 fn()와 같이 동일하게 작동하고 결괏값이 일정하다면 prototype속성에 fn()메서드를 추가한다면 객체가 생성될 때마다 fn()가 추가적으로 메모리에 저장되는 일은 피할 수 있을 것 같습니다.

프로토타입 상속

지금까지 프로토타입과 프로토타입 체인에 대해 알아보았습니다. 프로토타입 체인은 대부분의 브라우저가 알아서 처리하는 로직입니다. 그렇다면 직접 객체를 생성하고 상속을 하려면 어떻게 해야 할지 알아보겠습니다.

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

function Pandaman (age, name, gender) {
  this.age = age;
  this.name = name;
  this.gender = gender;
}

Pandaman.prototype.sayHello = function() {
 alert(`hi! my name is ${this.name}!`);
};

Pandaman함수를 생성했고, Pandaman의 프로토타입 속성에 sayHello()메서드를 추가했습니다.
이제 Pandaman를 상속받는 PandaBaby() 함수를 만들겠습니다.

function PandaBaby(age, name, gender, job) {
 Pandaman.call(this, age, name, gender);
 this.job = job;
};

const pandababy1 = new PandaBaby(10, 'baby', 'male', 'student');
pandababy1.age; // 10
pandababy1.job; // 'student'

PandaBaby함수와 Pandaman함수가 비슷하게 생겼지만, 차이점이 있습니다. call() 함수가 보이네요.
call()에서 첫 번째 인수의 this는 현재 객체(호출하는 객체)를 참조합니다. 그리고 나머지 인수에는 실제 함수 실행에 필요한 인자들이 전달됩니다. call()을 이용하면, 새 객체를 위해 새로 함수를 재작성할 필요 없이 다른 객체에 상속할 수 있습니다.

위의 예제는 PandaBabyPandaman를 상속받았기 때문에, call()함수를 사용하였고, 첫 번째 인수에 this, 나머지는 필요한 인수들을 전달했습니다. 그리고 PandaBaby()만의 속성 job을 추가했습니다. 마지막으로 pandababy1 인스턴스 객체를 생성했습니다. pandababy1Pandaman, PandaBaby속성에 접근한 것을 확인할 수 있습니다

프로토타입과 생성자 참조 설정하기

위의 상속과정에서 중요한 것이 빠졌습니다. 방금 정의한 새 생성자 PandaBaby에는 생성자 함수 자신에 대한 참조만 가지고 있는 프로토타입 속성이 할당되어 있습니다. 정작 상속받은 Pandaman생성자의 prototype 속성은 없죠.
위에서 Pandaman 프로토타입에 추가한 sayHello() 메서드를 Pandababy() 생성자 인스턴스인 pandababy1에서 호출할 수 있을까요? 호출할 수 없습니다. sayHello() 메서드도 상속받게 하기 위해서는 어떻게 해야 할지 살펴보겠습니다.

Pandababy.prototype = Object.create(Pandaman.prototype);

Pandababy 프로토타입 객체에 Pandaman 프로토타입 객체를 자신의 프로토타입으로 가지는 새로운 객체를 할당해주었습니다. 따라서 Pandaman.prototype에 정의된 메서드를 사용할 수 있습니다.
한 가지 더 해야 할 것이 있습니다. Pandababy.prototype.constructor가 Pandaman()으로 되어있습니다. 따라서 아래와 같이 변경할 수 있습니다.

Pandababy.prototype.constructor = Pandababy;

변경이 완료되었습니다.

const pandababy2 = new Pandababy(1, 'oyg0420', 'male', 'developer');
pandababy2.sayHello();

Pandababy()를 통해 pandababy2를 생성했습니다. 이제 pandababy2에서 sayHello() 메서드에 접근이 가능합니다.

es6 클래스

es6에서는 C++ 나 Java와 유사한 클래스 문법을 공개하여 클래스를 조금 더 쉽고 명확하게 재활용할 수 있게 되었습니다. - MDN -

이번 예제는 프로토타입 상속으로 작성한 PandamanPandabay예제를 클래스 문법으로 변경하고 어떻게 동작하는지 설명하겠습니다.

class Pandaman {
 constructor(age, name, gender) {
   this.age = age;
   this.name = name;
   this.gender = gender;
 }

 sayHello() {
  alert(`hi! my name is ${this.name}!`);
 };
}

class로 재작성한 Pandaman입니다. constructor메서드는 Pandaman클래스의 생성자를 의미합니다.
greeting()은 멤버 메서드입니다. 클래스의 메서드는 생성자 다음에 아무 메서드나 추가할 수 있습니다.
그리고 new 연산자로 객체 인스턴스를 생성할 수 있습니다.

let pandaman1 = new Pandaman(31, 'oyg0420', 'male');
pandaman1.sayHello();
pandaman1.age; // 31

class문법으로 상속

위의 작성한 Pandaman 클래스를 상속받은 Pandababy 클래스를 만들어보겠습니다. 이 작업을 하위 클래스 생성이라고 부릅니다.
하위 클래스를 만들기 위해서는 extends 키워드를 통해 상속받을 클래스를 명시해야 합니다.

class Pandababy extends Pandaman {
 constructor(age, name, gender, job) {
  super(age, name, gender);
  this.job = job;
 }
}

extends 키워드를 작성하여 Pandaman를 상속받는다는 것을 명시해주었습니다. super()연산자를 사용하면 상위 클래스의 멤버들을 상속받을 수 있습니다.

간단하게 상위 클래스의 멤버를 상속받는 방법에 대해 알 수 있었습니다.
이제 Pandaman클래스를 상속받은 Pandababy클래스의 인스턴스를 생성하여 Pandaman, Pandababy 메서드와 속성을 사용할 수 있습니다.

const pandababy = new Pandababy(3, 'oyg0420', 'male', 'developer');
pandababy.sayHello();
pandababy.age; // 3

Getters와 Setters

생성한 클래스 인스턴스의 속성 값을 변경하거나 최종 값을 예측할 수 없는 경우가 있을 겁니다. 이런 상황에 getter/setter가 필요합니다. - MDN -

Pandababy 클래스에 getter와 setter를 추가하겠습니다.
getter는 현재 값을 반환하고, setter는 해당하는 값을 변경합니다.

class Pandababy extends Pandaman {
 constructor(age, name, gender, job) {
  super(age, name, gender);
  this._job = job;
 }

 get job() {
  return this._job;
 }

 set job(newJob) {
  this._job = newJob;
 }
}

let pandababy = new Pandababy(3, 'oyg0420', 'male', 'developer');

pandababy._job = 'Product Manager';
console.log(pandababy._job); // 'Product Manager'

_job 속성 값을 보기 위해서는 pandababy._job를 실행합니다. _job을 수정하기 위해서는 pandababy._job = 'Product Manager'를 실행합니다.

지금까지 프로토타입과 상속에 대해 알아보았습니다. 읽어주셔서 감사합니다.

 

파트너스 활동을 통해 일정액의 수수료를 제공받을 수 있음

Comments