본문 바로가기

JavaScript

[ JavaScript ] 프로토타입(Prototype)

반응형

자바스크립트는 프로토타입 기반 객체지향 프그래밍 언어이다. 프로토타입을 공부하면 자바스크립트의 동작 원리를 더 잘 이해할 수 있다. 아마 자바스크립트를 공부해본 사람이라면 MDN 웹문서에서 ‘Prototype’이라는 단어를 많이 봤을 것이다. 나는 주로 MDN 웹문서를 기반으로 공부하기 때문에 ‘Prototype’이 굉장히 익숙한 단어였다. 익숙하기만 하고 뭔지 모르는.. 그런 단어였다. 그래서 ‘언젠가는 공부해야지'라고 생각만 하고 정작 (귀찮아서)정리 하지는 않았다. 하지만 더이상 미루면 평생 정리하지 않을 것 같아 오늘 굳은 다짐을 하고 책상에 앉았다. 오늘의 목표는 프로토타입을 정복하는 것이다!!!

 

프로토타입이란?

MDN 웹문서에는 아래와 같이 기술하고 있다.

Prototypes are the mechanism by which JavaScript objects inherit features from one another

 

프로토타입은 자바스크립트 객체가 서로 속성을 상속받는 메커니즘이다. 있는 그대로 받아들이면 된다. 핵심은 ‘상속'에 있다. 자바스크립트에서 어떤 객체는 반드시 부모 객체를 가지며, 이 부모 객체를 ‘프로토타입'이라고 한다. 그리고 자식 객체는 ‘__proto__’ 접근자를 통해 부모 객체, 즉 자신의 프로토타입에 접근할 수 있다. 상속 개념이기 때문에 자식 객체는 부모 객체의 모든 속성을 사용할 수 있다. 만약에 자식 객체에 없는 속성 또는 메소드를 호출했을때 자바스크립트는 내부적으로 자신의 부모 객체로 가서 찾는다. 부모 객체에 없으면 부모의 부모로 가고, 이 과정이 반복된다. 이러한 과정들을 ‘프로토타입 체이닝'이라고 한다. 모든 객체의 최상위 객체는 Object.prototype이며, 이 객체의 부모 객체는 null이다. 따라서 프로토타입 체이닝의 최종 종착지는 null이다. 간단한 예시를 하나 보자.

 

const obj = {
	a: 1,
	b: function() {}
}

console.log(obj.__proto__ === Object.prototype) // true
console.log(Object.prototype.__proto__ === null) // true

 

위 코드를 그림으로 나타내면 아래와 같다.

 

약간 딥하게 들어가보면, 위 그림에서 [[Prototype]]은 모든 객체에 존재하는 내부 슬롯이다. 이는 자바스크립트 엔진의 내부 속성으로 사용자가 직접적으로 접근할 수 없고, 오직 __proto__ 접근자로만 간접적으로 접근할 수 있다. [[Prototype]]은 자신의 프로토타입 객체를 참조하고 있다. 즉, 프로토타입 객체에 대한 참조값은 [[Prototype]] 내부 슬롯에 저장되어 있고, 이를 __proto__ 접근자로 접근할 수 있는 것이다. 그러면 자연스럽게 이런 생각이 든다. '왜 직접적으로 접근할 수 없는 것일까?' 이는 프로토타입 체인이 무한 루프를 도는 것을 방지하기 위함이라고 한다. 만약에 아래와 같이 두 객체를 생성하고 서로를 __proto__로 설정하면 에러가 난다. 만약에 __proto__를 거치지 않고 직접적으로 접근해서 수정한다면 에러가 나지 않고 무한 루프에 빠질 것이다. 

 

const a = {};
const b = {};

a.__proto__ = b;
b.__proto__ = a;

// TypeError: Cyclic __proto__ value

 

 

여기까지 꽤 간단해 보인다. 일반 객체만 놓고 보면 프로토타입은 어렵지 않은 개념이다. 그런데 ‘함수 객체'가 끼는 순간 많이 어려워진다. 아까 봤던 코드를 다시 보자. obj와 Object의 차이는 무엇일까? typeof로 찍어보면 차이를 알 수 있다. 그리고 obj에 ‘prototype’ 속성이 있는지도 확인해보자.

 

const obj = {
	a: 1,
	b: function() {}
}

console.log(typeof obj)    // object
console.log(typeof Object) // function
console.log(obj.prototype) // undefined

 

Object는 함수이며, obj 객체에는 prototype 속성이 없는 것을 확인할 수 있다. 자바스크립트에서는 함수도 객체인데 일반 객체와 구분짓는 이유는 무엇일까? 그야 당연히 함수 객체가 일반 객체와 약단 다른 점이 있기 때문이다. 따라서 먼저 이 둘이 어떻게 다른지 짚고 넘어가야 할 필요가 있다. 

 

함수 객체

함수 객체와 일반 객체의 차이점은 '호출 여부'에 있다. 일반 객체는 호출 불가능하지만, 함수는 호출 가능하다. 함수가 일반 객체와 다르게 호출 가능한 이유는 내부 메소드 [[Call]] 때문이다. 이는 자바스크립트 엔진의 내부 메소드로 [[Prototype]]과 비슷하다. 어떤 객체가 내부 메소드 [[Call]]이 존재하면 이를 '함수'라고 부르며, 호출 가능한 객체를 'callable'이라고 한다. 따라서 함수는 callable 하다. 

 

그런데 함수도 일반 함수와 생성자 함수로 구분된다. 이 둘을 구분짓는 요소는 내부 메소드 [[Construct]]의 존재여부로, [[Construct]]가 있는 함수는 생성자 함수로서 동작할 수 있다. [[Construct]]는 new와 함께 사용할 때 호출되는 내부 메소드다.  생성자 함수로서 동작할 수 있는 함수를 constructor라 부르고, 그렇지 않은 함수를 non-constructor라고 부른다. 정리하면 아래와 같다. 

 

- 모든 함수는 [[Call]] 이라는 내부 메소드를 가지고 있다.
- 생성자 함수는 [[Construct]] 라는 내부 메소드를 가지고 있다. 
- 일반 호출 : [[Call]] 메소드 호출
- new와 같이 호출 :  [[Construct]] 메소드 호출

- constructor : 함수 선언문, 함수 표현식, 클래스
- non-constructor: ES6 메소드 축약 표현, 화살표 함수

 

// 1. 함수 선언식
function f1() {}
new f1() // 가능


// 2. 함수 표현식
const f2 = function() {}
new f2() // 가능


// 3. 클래스
class f3{}
new f3() // 가능


// 4. ES6 메소드 축약 표현
const obj = {
    f4: function() {},
    f5(){}
}
new obj.f4() // 가능
new obj.f5() // 불가능


// 5. 화살표 함수
const f6 = () => {}
new f6() // 불가능

 

4번이 굉장히 흥미롭다. 객체의 메소드 표현 방식에 따라 의미가 달라지는건 몰랐던 사실이다. 쓸 일이 있을까 싶지만..ㅎㅎ 

 

함수 얘기가 어쩌다보니 좀 길어졌는데... 내가 하고 싶은 얘기는 딱 하나다. 아래 문장만 기억하고 있으면 된다. 

 

prototype 속성은 constructor(함수 선언문, 함수 표현식, 클래스)만 가진다!!

 

prototype의 정체

다시 프로토타입으로 돌아가자. 방금 prototpye 속성은 constructor만 가진다고했다. 앞으로 constructor를 함수 객체로 부르겠다. 함수 객체만이 소유하는 prototype 속성은 생성자 함수가 생성할 인스턴스의 프로토타입을 가리킨다. 그림으로 나타내면 아래와 같다. 

 

function Person() {
	this.prop = 'prop'
    this.method = function() {
    	console.log('method')
    }
}

const me = new Person();

 

이를 토대로 자바스크립트에서는 함수를 정의할때, 아래의 값들이 자동 세팅된다. 

 

1. 함수 객체에는 prototype 속성이 있다.

2. 함수 객체의 prototype 속성은 생성자 함수가 생성할 인스턴스의 프로토타입을 가리킨다.

3. 프로토타입의 constructor 속성은 다시 생성자 함수를 가리킨다. 

 

여기서 프로토타입의 정체가 밝혀진다. 프로토타입의 정체는 생성자 함수의 내부 속성값이다. 생성자 함수 자체가 아니라, 생성자 함수의 속성값임을 유의하길 바란다. 따라서 모~든 프로토타입의 형태는 아래와 같다. 

 

// 프로토타입 형태
생성자함수.prototype

// 아래는 반드시 성립한다
생성자함수.prototype.constructor === 생성자함수

 

 

지금까지 잘 이해했다면 아래의 명제 또한 이해할 수 있을 것이다.

 

  • 모든 객체는 프로토타입을 가진다.
  • 프로토타입의 형태는 '생성자함수.prototpye' 꼴이다. 
  • 프로토타입은 constructor 속성에 의해 생성자 함수와 연결되어 있다.
  • 결론 → 모든 객체는 생성자 함수와 연결되어 있다.

 

 

생성자 함수의 속성과 프로토타입 속성은 다르다는 것을 잊지 말자. 아래 그림은 MDN 웹문서의 캡쳐본이다. Array.prototpye.indexOf()는 생성자 함수의 모든 인스턴스에 적용되는 메소드고, Array.isArray()는 생성자 함수 자체의 메소드다. 클래스의 static과 똑같다. 

const ary = [];

ary.indexOf();     // 가능
ary.isArray();     // 불가능
Array.isArray(ary) // 가능

 

원시타입과 프로토타입

자바스크립트에서 원시타입을 제외한 모든 것은 객체다. 따라서 원시타입은 프로토타입이 없을 것으로 예상된다. 아래 코드를 실행시켜보자. 

 

'string'.at(1)

 

놀랍게도 실행 된다...!! MDN 웹문서에 따르면 null, undefined 타입을 제외한 모든 원시 타입은 생성자 함수에 의해 랩핑(wrapping) 된다고 한다. 즉, 리터럴 방식으로 정의해도 자바스크립트 엔진이 생성자 함수로 감싸주기 때문에 결론적으로 모든 것은 객체가 되는 것이다. 원시 타입이 일반 객체와 구별되는 점은 'immutable' 특성 뿐이다. 각 원시 타입은 아래의 생성자 함수로 랩핑된다. 

 

  • String for the string primitive
  • Number for the number primitive
  • BigInt for the bigint primitive
  • Boolean for the boolean primitive
  • Symbol for the symbol primitive

 

 

<참조>

https://developer.mozilla.org/en-US/docs/Glossary/Primitive

https://poiemaweb.com/js-prototype

 

반응형