프로토타입을 이해해야 자바스크립트를 이해할 수 있다

자바스크립트에는 Class가 없다..??

무슨 소린가 싶다…
분명 class를 사용했던 기억이 있는데 자바스크립트에는 class가 없다니..?

결론부터 말하자면, 자바스크립트는 프로토타입 기반 언어다. 자바스크립트에는 클래스 개념이 아예 존재하지 않기 때문에, 프로토타입을 통해 클래스를 흉내 내는(?) 방식을 따른다는 것이다. 물론 여기서 혹자는 이렇게 되물을 수도 있다.

“분명 ES6에서 클래스가 추가됐는데 그럼 이건 뭐죠?”

ES6에서 클래스라는 ‘문법’이 추가됐을 뿐, 자바스크립트가 클래스기반 언어로 변경된 것은 아니다. 즉, 자바스크립트는 여전히 프로토타입 기반 객체지향 언어이며, 클래스를 ‘흉내’낼 뿐이다.

뭐 일단은 알겠는데… 그래서 프로토타입은 뭐고, 클래스를 흉내낸다는 것은 뭘까?

클래스를 흉내내는 법

ES6 이전에 우리는 생성자 함수를 통해 아래와 같이 클래스를 ‘흉내’냈었다.

1
2
3
4
5
6
7
8
9
10
// 이 함수를 통해 생성된 객체는head=1, eyes=2라는 프로퍼티를 할당 받는다.
function Person() {
this.head = 1;
this.eyes = 2;
}

var park = new Person();

console.log(park.head); // 1
console.log(park.eyes); // 2

꽤나 직관적이고 간단한 방법이라고 할 수 있다. 하지만, 보기와는 다르게 이 방법에는 공간 비효율성이 존재한다.

현재는 하나의 Person 객체만 생성했을 뿐이지만, 만약 100개 1,000개의 Person 객체를 생성해야 한다면… 각 객체들에는 두 변수(head, eyes)가 전부 할당되기에 200개 2,000개의 변수가 생성되는 비효율성을 초래할 수 있기 때문이다.

1
2
3
4
5
6
7
8
9
var one = new Person();
var tow = new Person();
var three = new Person();
var four = new Person();
var five = new Person();
// ... x1000

// 이런식으로 객체가 많이 생성될 경우,
// 객체에는 각각 두 개의 변수가 계속 할당되는 공간적 비효율성이 발생할 수 있다.

프로토타입의 활용성

위의 비효율성 문제에서 프로토타입의 필요성이 드러난다..!!

아래와 같이 프로토타입을 활용하며, 객체 생성 시 변수의 반복적 할당을 피할 수 있기 때문이다. 즉, 프로퍼티에 자주 쓰이는 공통 프로퍼티나 메소드를 담아두고, 연결된 자식 객체들에게 이를 상속해줄 수 있다.

1
2
3
4
5
6
7
8
9
function Person() {}

const park = new Person();

// Person 함수 자체에 할당되는 것이 아닌, 프로토타입에 할당이 되는 것!
Person.prototype.head = 1;
Person.prototype.eyes = 2;

console.log(park.head, park.eyes); // 1 2

그래서 프로토타입이 뭔데요..

위의 코드와 프로토타입에 대해서는 아래에서 다시 설명할테니, 그냥 “그렇구나..” 정도로만 이해하고 넘어가도 된다😅


그래서 프로토타입이 대체 뭘까?

프로토타입은 하나의 ‘객체’로, 모든 객체들의 원형이자 부모라고 할 수 있다.
그리고 이 원형은 공통으로 사용할 수 있는 프로퍼티와 메소드를 저장하여 상속해주는 역할을 한다.

부모 객체라고 할 수 있는 프로토타입으로부터 프로퍼티와 메소드를 상속 받는다.

프로토타입이라는 ‘단어’의 사전적 의미는 ‘원형’이다. 이는 곧, 어떠한 것의 원래 모양 또는 원래의 형태라는 것이다. 따라서 자바스크립트에서의 프로토타입은 객체의 원형 또는, 부모 객체라고 이해하면 된다. 우리는 이러한 프로토타입 객체로부터 다양한 프로퍼티 또는 메소드들을 상속 받아 사용할 수 있다.

이러한 프로토타입 객체는 [[Prototype]]으로 표기된다. 그리고 이를 우리는 인터널 슬롯(Internal Slot) 이라고 부르기도 한다.

프로토타입 객체 = [[Prototype]] = 인터널 슬롯(Internal Slot)

“object that provides shared properties for other objects”
”프로토타입은 공유된 프로퍼티를 다른 객체에 제공하는 하나의 객체다.” (ECMEScript-262)

프로토타입에 커스텀 프로퍼티 및 메소드를 생성하는 법

그럼 다시 위에서 다뤘던 코드를 통해 [[Prototype]]에 특정 프로퍼티와 메소드를 넣어주는 과정을 다시 봐보도록 하자.

1
2
3
4
5
6
7
8
9
10
11
12
function Person() {}

const me = new Person();

// Person의 프로토타입(원형)에 head라는 프로퍼티와 sayHello라는 메소드를 추가해준다.
Person.prototype.head = 1;
Person.prototype.sayHello = function () {
return 'Hello!!';
};

console.log(me.head); // 1이 반환된다.
console.log(me.sayHello()); // Hello!!

그러면 아래와 같이 프로토타입에 eyes와 sayHello가 각각 잘 저장된 것을 확인할 수 있다.

이러한 기본적인 활용법을 토대로 우리는 객체와 객체 간의 연결, 그리고 프로퍼티 또는 메소드의 상속을 구현할 수 있는 것이다.

프로토타입 체인 (Prototype Chain)

근데… 위의 코드를 보면 조금 이상하다.
분명 Person 함수는 텅 비어있는데 어떻게 me.head가 1을 반환할 수 있었던 걸까?

결론부터 말하자면, 프로토타입 체인이 적용됐기 때문인데.. 아래와 같은 로직을 따른다.

1
2
3
4
5
6
7
8
9
10
// 1.분명 생성자 함수에는 head가 없다.. 텅 비어있는 모습이다.
function Person() {}
// 2.head와 eyes는 프로토타입에 할당됐다. Person 함수에 직접적으로 할당된게 아니다..
Person.prototype.head = 1;
Person.prototype.eyes = 2;

const me = new Person();

// 3.근데 me.head를 찍으니 1이 반환된 모습이다. 이는 프로토타입 체인이 적용됐기 때문이다.
console.log(me.head); // 1

프로토타입 체인은 뒤에서 나올 Prototype Link와 연결되는 개념이니, 반드시 짚고 넘어가도록 하자.

즉, Person.prototype에 우리가 head와 eyes를 생성/할당 해놓았기 때문에, 이를 프로퍼티 체인을 통해 상속 받아 사용할 수 있는 것이다.

위 그림과 연관지어 좀 더 자세히 설명하자면 이하와 같다.


  • 자바스크립트는 우선적으로 me 객체에서 head를 찾는다.
  • me에서 찾지 못하면, me 객체의 __proto__라는 내부 프로퍼티를 통해 자신의 부모인 Person의 프로토타입 객체에 접근한다. (__proto__는 뒤에서 설명할 예정)
  • 그리고 거기에서 head를 찾는다.
  • 그렇게 Person의 프로토타입 객체에 존재하는 head 값을 찾아 반환 해준다.

이처럼 프로토타입을 통해 객체가 연결되기 때문에, 이를 프로토타입 체인이라고 하는 것이다.

중간 정리

여기까지 프로토타입의 기본적인 개념과 간단한 활용법에 대해 다뤘다.
이를 다시 간단하게 요약해보도록 하겠다.

  • 자바스크립트는 프로토타입 기반 언어이기 때문에 클래스를 사용할 수 없다. 하지만 프로토타입의 특성을 통해 우리는 클래스를 흉내낼 수 있는 것이다.
  • 프로토타입이란, 하나의 객체이자 모든 객체들의 원형이다. 이 프로토타입은 생성자 함수를 통해 생성된 객체들에게 공통의 프로퍼티 및 메소드를 제공한다. (공통 프로퍼티 = 위에서의 head, eyes)

더 깊게 이해하기 : Prototype Object

뭐 사실.. “프로토타입은 공통의 프로퍼티를 상속해주기 위해 활용되는 객체다” 정도만 이해해도 되겠지만, 우리는 자바스크립트라는 언어를 통해 밥을 벌어 먹어야 하는 사람들이다…

따라서 우리는 자바스크립트의 핵심 개념이자 근간이라고 할 수 있는 프로토타입에 대해 더 깊이, 자세히 공부해야 할 필요가 있다. 우선 프로토타입은 객체라고 했으니, 자바스크립트에서 객체가 어떻게 생성되는지 먼저 살펴보도록 하자.

객체는 어떻게 생성되는가?

자바스크립트에서 객체를 생성하는 방식은 이하와 같다.

1
2
3
4
5
6
7
function Person() {}

const personObj = new Person(); // 함수를 통해 객체를 생성한 모습

const obj = new Object(); // new Object()는 자바스크립트에 내장된 기본 객체 생성함수다.

const obj = {}; // 자바스크립트에서는 이러한 리터럴을 new Object()와 동일하게 인식한다.

이처럼, 모든 객체는 함수로부터 생성된다. 따라서, 함수의 생성 과정과 연결 지어 객체가 어떻게 생성되는지, 프로토타입은 어떻게 구성되는지 설명해보도록 하겠다. 이러한 과정 속에서 Prototype Object의 개념을 자연스럽게 이해할 수 있을 것이다.

함수와 함께 생성되는 것들 (prototype, Prototype Object)

위의 사진과 같이, 특정 함수가 생성됐을 때 prototype 프로퍼티Prototype Object([[Prototype]]) 이 함께 세트로 생성된다.

그리고 함수의 프로토타입 프로퍼티, Prototype Object 간의 연결 관계는 위와 같다.

즉, constructor는 생성자 함수를, prototype 프로퍼티는 Prototype Object([[Prototype]])와 연결되어 있는 관계인 것이다.

constructor

더 나아가, 위 사진에서 유심히 봐야 할 것이 constructor다.
이 constructor와 [[Construct]]라는 메소드가 조금 헷갈릴 수 있기 때문이다.

먼저, constructor 프로퍼티는 자신을 생성한 함수객체 자체를 가리킨다. 즉, constructor는 객체에 존재하는 하나의 프로퍼티인 것이다.

하지만, 이와 달리 [[Construct]]는 프로퍼티가 아닌 메소드다. 다음 포스팅에서 자세히 다루겠지만, 우선은 이 메소드가 존재해야 함수는 생성자로서의 자격이 생긴다고만 이해해도 충분하다.
따라서 그냥 일반적인 오브젝트는 함수가 아니기 때문에 new 를 통해 객체를 생성해낼 수 없다. (아래 사진 참고)

다시 정리하자면, 함수는 생성자로서의 자격을 부여받을 수 있는데, 이 때 필요한 것이 [[Construct]]메소드다. 그리고 모든 객체는 자신을 생성한 함수객체를 가리키는 constructor라는 프로퍼티를 보유한다. (다음 포스트에서 다루겠지만, 화살표 함수와 메소드 함수는 생성자 자격을 부여받지 못한다)

즉, 생성자 함수의 자격을 부여하는 것은 [[Construct]]메소드, 객체의 생성자 함수객체를 가리키는 것은 constructor프로퍼티인 것이다.


초반부에서 설명한 프로토타입 체인과 연결되는 부분이자, 프로토타입의 또 다른 핵심 개념 중 하나라고 할 수 있다.

모든 객체에는 __proto__라는 프로퍼티가 존재하는데, 이 프로퍼티를 통해 프로토타입 체인이 적용될 수 있다. 그리고 이 __proto__는 자신의 부모 객체의 프로토타입을 가리킨다. 다시 한 번 그림을 통해 확인해보도록 하자.

1
2
3
4
5
6
7
8
9
10
// 1.함수를 생성한다.
// 그러면 Prototype Object와 prototype 프로퍼티가 같이 생성된다.
function Person() {}
Person.prototype.eyes = 2;

// 2.Person 생성자 함수를 통해 객체를 me에 생성,할당해준다.
const me = new Person();

// 3.me.eyes를 찍어본다.
console.log(me.eyes); // 그러면 me의 __proto__를 통해 Person의 Prototype Object에 접근한다.

마지막으로, 만약 Person의 Prototype Object에도 찾고자 하는 프로퍼티나 메소드가 존재하지 않는다면, 모든 객체의 최상위 객체라고 할 수 있는 Object의 Prototype Object까지 접근해서 검색하고, 없다면 undefined를 반환한다.

조금 헷갈릴 수도 있는데, 함수도 객체이므로 Function.prototype.__proto__는 자신을 생성해준 객체(Object)의 프로토타입을 가리키게 돼있다.

그리고 ECMAScript에서도 이 부분을 명확히 설명한다.

”Function Prototype Object has a [[Prototype]] internal slot whose value is %Object.prototype%.” (ECMAScript-262)

결국 함수도 객체이고, 함수 객체의 최상위(조상) 프로토타입 객체는 Object라는 것이다. (이 부분은 다음 포스팅에서 자세히 다뤄보도록 하겠다)


프로토타입 객체 vs 프로토타입 프로퍼티

함수 또한 객체다.
따라서 함수도 마찬가지로 인터널 슬롯([[Prototype]])을 보유한다.

함수 Apple에는 prototype이 잘 실행되지만, greenApple이란 객체에는 undefined가 반환된다.

위 사진처럼 함수 객체에 .prototype을 찍어보면, 함수 자신의 프로토타입 객체가 반환되는 것을 확인할 수 있다. 하지만.. 또 일반 객체에서는 undefined가 반환된다.🙊

이러한 차이점이 발생하는 이유는, prototype프로퍼티는 함수에만 존재하기 때문이다. 한 객체 안에서 프로토타입 프로퍼티와 프로토타입 오브젝트가 가리키는 결과물은 동일하지만, 그 용도와 관점에 있어서는 차이점을 드러낸다.

프로토타입 객체와 prototype 프로퍼티의 차이점!

prototype 프로퍼티는 자기 자신의 프로토타입 객체를 가리킨다.

말 장난같이 들릴 수도 있고, 헷갈릴 수도 있다… 하지만 그게 전부다. 아래의 그림을 살펴보자.

결과적으로, Apple.property를 찍으면 생성자 함수 Apple의 프로토타입 객체가 반환된다는 의미다. 그리고 Apple의 자식이라고 할 수 있는 greenApple 객체의 __proto__는 자신의 부모인 Apple의 프로토타입 객체를 가리킨다.

즉, 생성자 함수에서의 prototype 프로퍼티는 자기 자신의 프로토타입 객체와 같다. (재차 강조하지만, 가리키는 대상은 같지만 용도와 관점 측면에서의 차이점 존재)

이를 다시 코드로 표현해보면,

1
2
3
4
5
console.log(Apple.prototype === greenApple.__proto__); // true

// 그리고 이와 같은 개념으로써,
// 생성자 함수 Apple의 __proto__는 자신의 부모인 원시함수 Function의 prototype과 같다고 할 수 있다.
console.log(Apple.__proto__ === Function.prototype); // true

정리 표


복습 퀴즈!

본문을 모두 정독했다면, 아래 퀴즈들을 통해 이해한 내용들을 다시 점검해보도록 하자 :)

1
2
3
4
5
6
7
8
9
// 예제 코드

function Person() {}

const friend = {};

Person.prototype.head = 1;

const me = new Person();
console.log(me.head)
= `1` → 프로토타입 체인이 적용된다. me의 __proto__ 를 통해 생성자 함수의 프로토타입에 접근하여 head라는 프로퍼티의 값을 get 해온다.
console.log(me.eye)
= undefined → me에도, Person에도, 심지어 Object에도 eye는 존재하지 않기 때문
console.log(me.__proto__ === Person.prototype)
= true → Person함수의 prototype프로퍼티는 Person의 프로토타입 객체를 가리킨다. 그리고 me의 프로토는 자신을 생성한 생성자 함수의 프로토타입 객체를 가리킨다.
console.log(friend.__proto__ === Object.constructor)
= false → friend는 new Object()에 의해 생성됐다. 따라서 friend의 __proto__는 Object() 생성자 함수의 프로토타입 객체를 가리킨다.
console.log(friend.prototype)의 반환 결과는?
= undefined → friend 자체는 객체다. 이는 함수가 아니기 때문에 prototype 프로퍼티를 보유하지 않는다.
console.log(me.constructor)
= Person(){} → constructor는 자신을 생성한 생성자(객체)를 가리킨다.
console.log(friend.constructor)
= Object(){} → 위와 동일하다.
console.log(friend.__proto__ === Object.prototype)
= true → 위에서 설명했던 바와 동일하다.
console.log(Person.__proto__ === Function.constructor)
= false → Person의 프로토는 원시 Function의 프로토타입 객체를 가리킨다. 하지만 Function.constructor는 Function 원시함수를 생성한 생성자 객체를 가리키기에 위의 문제는 오답이다.
console.log(Person.prototype.constructor)
= Person(){} → Person의 prototype 프로퍼티는 Person 자신의 프로토타입 객체를 가리킨다. 따라서 자기 자신의 프로토타입 객체의 생성자 함수는 Person()이기 때문에 답은 Person이 된다.
console.log(Function.prototype.__proto__)
= Object.prototype과 같다. → Function의 prototype은 자기 자신의 프로토타입 객체를 가리킨다. 그리고 그 프로토타입 객체의 프로토는 자신을 생성한 생성자의 프로토타입 객체와 연결된다. Function은 Object의 하위요소이기 때문에 정답은 Object의 프로토타입 객체를 가리키는 Object.prototype이다.

참조한 자료

[Javascript ] 프로토타입 이해하기

Prototype | PoiemaWeb

ECMAScript® 2020 Language Specification


프로토타입을 이해해야 자바스크립트를 이해할 수 있다

https://hoonjoo-park.github.io/javascript/base/prototype/

Author

Hoonjoo

Posted on

2022-03-29

Updated on

2022-04-07

Licensed under

Comments