[JS] Prototype 개념이해

colki·2021년 3월 22일
0
post-custom-banner

prototype

자바스크립트는 프로토타입 기반의 언어로써, 원본 객체를 기반으로 이를 복제하여 상속과 비슷한 기능을 구현할 수 있다.
프로토타입이란 자바스크립트의 모든 함수가 가지고 있는 각각의 고유한 속성이면서 동시에 자식 객체들이 자신을 참조할 수 있도록 만든 분신 객체이다.
(생성자)함수는 알고보면 함수 자기자신 + 프로토타입객체 2개가 세트인 것이다.
아직은 무슨말인지 이해하기가 어려울 것이다.
(난 이 문장을 이 포스팅을 수정하면서 가장 마지막에 쓰고 있다.😁 이제서야 이해했다는 말이다.)

const object = {};
const array = [];
const function = function () { };
function foo() [ }

먼저 객체들이 만들어지는 방법을 확인해보자.
일반적으로 중괄호, 대괄호, 함수선언식 또는 함수표현식을 사용해서 간단히 만든다.

const object = new Object();
const array = new Array();
const function = new Function();

그런데 우리가 쉽게 객체를 만들고 있을 때,
사실 자바스크립트는 백그라운드에서 '생성자함수'를 이용해서 객체를 만들어내고 있다.
그러니까 객체는 모두 함수로 만들어지는 것이었다.



const instance = new Constructor

모든 함수가 프로토타입을 가지고 있지만,
생성자 함수로 인스턴스를 만들고 실행할 때만 프로토타입객체가 특별한 기능을 수행하게 된다.
new 키워드 없이 일반함수를 호출하는 경우에는 프로토타입은 아무 기능을 하지 않는다.
(new 키워드는 함수 앞에만 붙을 수 있다.)

프로토타입에 대해 자세히 알아보면 다음과 같다.

Constructor.prototype

그림에서 알 수 있듯이 생성자함수와 프로토타입객체는 서로를 가리키는 끈끈한 관계이다.

prototype 이란!?

  • 모~든 함수 객체만 가지고 있는 프로퍼티이며 동시에 객체이기도 하다.
    (함수 생성방식과 관계없이 모든 함수가 가지고 있다.)

  • 함수 앞에 new키워드를 붙여서 생성자 함수를 정의할 때, prototype 객체도 동시에 만들어진다.
    이것은 부모 객체 Constructor을 가리킨다.

  • 자바스크립트의 모든 인스턴스 객체는 부모 객체의 프로토타입에 존재하는 속성 또는 메소드를 사용할 수 있다.
    - 인스턴스가 생성되는 당시에 이미 부모가 가지고 있던 속성을 사용하는 건 상속받는 것이지만,
    - 인스턴스를 만든 이후 부모 객체가 속성을 추가했다면 자식 인스턴스는 부모의 속성을 공유한다고 말할 수 있다.

  • prototype에는 constructor 프로퍼티가 있는데 이것은 생성자함수를 가리킨다.
    또한 모든객체는 __proto__라는 프로퍼티가 있는데, 이는 자신의 상위객체의 prototype을 가리킨다.
    Constructor.prototype.constructor & instance.__prot__
    이것이 바로 프로토타입 기반의 상속에서의 중요한 특성이다.

일단 생성자함수가 생기면 prototype이라는 속성/객체가 생긴다는 정도로 이해하고 다음으로 넘어가자.



.prototype & .__proto__

생활코딩 이고잉님의 prototype vs proto 유튜브 강의 를 참고해서 그린
생성자함수와 인스턴스의 __proto__가 prototype을 참조하는 구조를 나타낸 그림이다.

생성자함수로 인스턴스 객체를 만들 때 __proto__속성을 포함한 프로퍼티들이 생성된다.
prototype 속성은 함수만 가지고 있던 것과는 달리 proto속성은 모든 객체가 빠짐없이 가지고 있는 속성이다.
(이 부분이 이해가 잘 안됐었는데, 그림으로 표현해보니 갑자기 이해가 되는 순간이 있었다 :D Good)

인스턴스 객체 내에서 속성을 찾지 못할 경우, proto가 참조하는 prototype객체에서 해당 속성이 있는지 탐색한다. 만약 없다면 undefined를 리턴하고, 있다면 그 속성과(key) 값을 받아올 수 있는 것이다.
쉽게 생각하면 __proto__는 prototype으로 가는 일종의 링크인 것이다.
그림에서 표현하지는 않았지만 constructor.prototype또한 객체이기 때문에 __proto__속성을 가지고 있다. 그렇게 상위 객체로 타고 타고 올라간다. 이렇게 proto속성을 통해 상위 프로토타입과 연결되어있는 형태가 바로 프로토타입 체인이다.
프로토타입 체인의 꼭대기에는 Object객체의 Object.prototype이 자리하고 있기 때문에, 모든 객체는 Object Prototype 에 존재하는 모든 속성을 사용할 수 있다. Object.prototype이 모두의 부모인 것이다.

Object가 아담이라면 Object.prototype은 하와?!!

this 포스팅에서도 다뤘었지만, 예제를 들어서 위 과정을 순서대로 짚어보겠다.

var Fruit = function(name, color) {
  this.name = name;
  this.color = color;
};


var banana = new Fruit('바나나', 'yellow'); // banana 인스턴스 생성
var strawberry = new Fruit('딸기'); // strawberry 인스턴스 생성


console.log(banana); //Fruit {name: "바나나", color: "yellow"}

console.log(strawberry); // Fruit {name: "딸기", color: undefined}
  1. new키워드를 붙여서 생성자함수(Constructor)를 호출한다.
    이때 고유의 프로토타입 객체도 함께 생성된다. ( Constructor.prototype )
new Fruit(); 
Fruit.prototype

  1. ' 빈객체 = { } = 새로운 instance ' 가 만들어진다.
banana는 Fruit의 인스턴스이다.

3. instance 또한 생성자함수가 prototype이라는 속성을 가지는 것처럼, `__proto__` 이라는 명칭을 가진 속성이 자동으로 생긴다. 이건 별개의 객체로 만들어지지 않는다.

  1. Constructor.prototype === instance.__proto__*

instance.__proto__은 자바스크립트 규칙에 따라서 부모격인 Constructor.prototype을 참조하기 때문에 prototype객체의 메서드나 속성에 접근해서 자기것인양 사용할 수 있다.
instance.__proto__Constructor.prototype은 결국 생성자함수를 가리키므로 ===연산자를 사용해서 비교하면 true값이 나온다.


*--- Constructor.prototype ---*

console.log(Fruit.prototype); 
/* {constructor: ƒ}
 ▶ constructor: ƒ (name, color)
 ▶ __proto__: Object
*/

*----- instance.__proto__ -----*

console.log(banana.__proto__); 
console.log(strawberry.__proto__);
/* {constructor: ƒ}
 ▶ constructor: ƒ (name, color)
 ▶ __proto__: Object
*/

__proto__는 생략하고 사용하며, 자식이 부모를 가리키는 이 속성은 사용하지 않는다.

banana에는 name, color라는 프로퍼티(속성)를 직접적으로 만든적은 없었지만
prototype을 프로퍼티, 메서드를 참조하므로 다음과 같이 동일한 프로퍼티를 갖게 된다.

  • (다만 함수 자체의 name속성은 있기 때문에 Fruit.name은 함수 자신의 고유한 이름 Fruit이다.Fruit.name = Fruit
    이외에도 생성자.속성 이렇게 prototype을 사이에 끼지 않고 정의하면 그것은 이 함수의 개인적인 속성이라 유전되지 않는 것이다. (그냥 부모의 성격이라고 생각하면 된다)

Fruit.prototype.name  → name이라는 프로퍼티를 가리킴
Fruit.prototype.color → color라는 프로퍼티를 가리킴

banana((__proto__)).name === banana.name → name이라는 프로퍼티를 가리킴
// '바나나'
banana((__proto__)).color === banana.color → color라는 프로퍼티를 가리킴
// 'yellow'

이때, name과 color 의 this는 Fruit.가 아니라 .앞의 banana((proto)), banana((proto)) 이 된다.
Fruit.prototype.name === banana.name ▶ 같은 속성을 사용한다.


Point

new 연산자로 Constructor을 호출하면 instance가 만들어지는데,
이 instance의 생략 가능한 프로퍼티인 __proto__는 Constructor의 prototype을 참조한다.

아빠 이름이 생성자, 엄마 이름은 프로토타입, 아들 이름은 인스턴스인 일가족이 있다.
엄마와 아빠는 서로를 아주 사랑해서, 서로의 이름을 각인한 물건을 각각 몸에 지니고 있다.
아들 인스턴스가 가지고 있는 물건중에 프로토라는 핸드폰이 있는데 여기엔 엄마이름만 저장 되어있어서 오로지 엄마를 부르는 용도로만 사용할 수 있다..
그리고 아들은 엄마의 물건을 물려받아서 맘대로 쓸 수 있고, 또 엄마가 새로 사는 물건들도
같이 사용할 수 있다.


인스턴스에게 해당 속성이 있다면 자신 객체에서 정의되어있는 key(속성)와 값 쓰지만 없다면 prototype의 key(속성)와 값을 갖다 쓴다.

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

Person.prototype.age = 20;

const colki = new Person("colki");

console.log(colki.age); // 20

colki 인스턴스에는 age라는 프로퍼티가 없지만,
Person.prototype을 참조하기 때문에 거기에 있는 속성을 찾아내서 사용한 것이다.
그래서 age의 값을 출력할 수 있다.

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

Person.prototype.age = 20;

const colki = new Person("colki");

** 하지만 age라는 key에 대한 값을 준다면 **
colki.age = 24;

console.log(colki.age); // 24

자신이 가지고 있는 속성이라면 굳이 prototype을 참조하지 않고 값을 출력할 것이다.
하지만 Person.prototype.age = 20;구문이 나중에 나온다면
console.log(colki.age); // undefined가 나올것이다.

😐 추가로 착각하기 쉬운 게 있는데

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

Person.age = 20;

const colki = new Person("colki");
console.log(colki.age); // --- ?

이렇게 있으면 무의식적으로 20! 이라고 외치기 딱 좋은데, 다시 들여다보자
Person. age 사이가 허전하다. 맞다 prototype 이 빠져있다.
그냥 객체/점/속성 으로 추가하면 생성자만이 단독으로 갖는 생성자만의 고유 속성을 지정해주는 거라, 인스턴스은 그 속성을 사용하지 못한다.

그렇기 때문에 위 추가 예제에서는 콘솔창에 undefined가 출력된다.

자식에게 상속시킬 목적이라면 속성을 추가할 때는 반드시 constructor.prototype.property 형태로 작성해야 한다.

profile
매일 성장하는 프론트엔드 개발자
post-custom-banner

0개의 댓글