코딩쌀롱

[코어 자바스크립트] 6-1. 프로토타입의 개념 이해 본문

[코어 자바스크립트] 6-1. 프로토타입의 개념 이해

이브✱ 2020. 12. 30. 15:40

 자바스크립트는 프로토타입 기반 언어이다. 클래스 기반 언어에서는 '상속'을 사용하지만 프로토타입 기반 언어에서는 어떤 객체를 원형(prototype)으로 삼고 이를 복제(참조)함으로써 상속과 비슷한 효과를 얻는다.

Constructor, prototype, instance

프로토타입을 도식으로 추상화 해본다면,

const instance = new Constructor();

〔코어 자바스크립트〕 책에 있는 도식을 참고해 그렸습니다.

1. 어떤 생성자 함수(Constructor)를 new 연산자와 함께 호출하면

2. Constructor에서 정의된 내용을 바탕으로 새로운 인스턴스(instance)가 생성된다.

3. 이때 instance에는 __proto__라는 프로퍼티가 자동으로 부여되는데,

4. 이 프로퍼티는 Constructor의 prototype이라는 프로퍼티를 참조한다.

 

prototype과 __proto__

prototype은 객체이다. 이를 참조하는 __proto__ 역시 당연히 객체. prototype 객체 내부에는 인스턴스가 사용할 메서드를 저장한다. 그러면 인스턴스에서도 프로퍼티인 __proto__를 통해 이 메서드들에 접근할 수 있게 된다.

 

예를 들어, Person이라는 생성자 함수의 prototype에 getName이라는 메서드를 지정했다고 해보자.

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

Person.prototype.getName = function() {
   return this._name;
};

const suzi = new Person('Suzi');
suzi.__proto__.getName();  // undefined

 Person의 인스턴스는 __proto__ 프로퍼티를 통해 getName을 호출할 수 있다. 왜냐하면 instance의 __proto__가 Constructor의 prototype프로퍼티를 참조하므로 결국 둘은 같은 객체를 바라보기 때문이다.

 그러나 메서드 호출 결과로 undefined가 나왔다. 문제는 this에 있다. 어떤 함수를 '메서드로서' 호출할 때는 메서드명 바로 앞의 객체가 곧 this가 된다. 따라서 suzi.__proto__.getName() 에서 getName 함수 내부에서의 this는 suzi가 아니라 suzi.__proto__라는 객체가 되는 것이다. 이 객체 내부에는 name 프로퍼티가 없으므로 undefined를 반환한 것이다.

 

const suzi = new Person('Suzi');
suzi.getName();   // Suzi
const iu = new Person('Jieun');
iu.getName();     // Jieun

__proto__를 빼면 this는 instance가 되는 게 맞지만, 이대로 메서드를 호출되고 원하는 값이 나오는 이유는 __proto__가 생략 가능한 프로퍼티이기 때문이다. 원래부터 생략 가능하도록 정의돼 있다. __proto__를 생략하면, suzi.__proto__에 있는 메서드를 사용할 수 있고, this는 suzi를 바라보게 할 수 있는 것이다.

 

다시 그림으로 표현하자면,

〔코어 자바스크립트〕 책에 있는 도식을 참고해 그렸습니다.

(.__proto__)를 생략하고, instance에 바로 메서드를 호출하더라도 가능하다. 그리고 this는 instance를 가리킨다!

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

 

 정리하면, 자바스크립트는 함수에 자동으로 객체인 prototype 프로퍼티를 생성해 놓는데, 해당 함수를 생성자 함수로 사용할 경우(new 연산자와 함께 호출), 그로부터 생성된 인스턴스에는 숨겨진 프로퍼티인 __proto__가 자동으로 생성되며, 이 프로퍼티는 생성자 함수의 prototype 프로퍼티를 참조한다. __proto__ 프로퍼티는 생략 가능하도록 구현돼 있기 때문에 생성자 함수의 prototype에 어떤 메서드나 프로퍼티가 있다면 인스턴스에서도 마치 자신의 것처럼 해당 메서드나 프로퍼티에 접근할 수 있게 된다.

 

배열 리터럴과 Array의 관계

내장 생성자 함수 Array를 바탕으로 한 번 살펴보자.

const arr = [1, 2];
console.dir(arr);
console.dir(Array);

크롬의 콘솔에서 확인한 배열 리터럴과 Array에 대한 출력 결과 일부이다.

  왼쪽은 arr 변수 출력, 오른쪽은 생성자 함수인 Array를 출력한 것이다. 왼쪽의 첫 줄에 Array(2)는 Array라는 생성자 함수를 원형으로 삼아 생성됐고, length가 2임을 알려준다. __proto__에는 배열에 사용하는 다양한 메서드들이 들어있다.

  오른쪽에는 함수라는 의미의 f가 표시돼 있고, 함수의 기본 프로퍼티들인 arguments, caller, length, name 등이 있다. 그리고 Array 함수의 스태틱 메서드인 from, isArray, of 등도 있다. prototype을 열어보니 왼쪽의 __proto__와 완전히 동일한 내용으로 구성 돼 있다. 위 출력 결과를 도식으로 구체화하면,

〔코어 자바스크립트〕 책에 있는 도식을 참고해 그렸습니다.

 Array를 new 연산자와 함께 호출해서 인스턴스를 생성하든, 그냥 배열 리터럴을 생성하든, instance인 [1, 2]가 만들어진다.  이 인스턴스의 __proto__는 Array.prototype을 참조하는데, __proto__가 생략 가능하도록 설계돼 있기 때문에 인스턴스가 push, pop 등의 메서드를 마치 자신의 것처럼 호출할 수 있다. 

 Array의 prototype 프로퍼티 내부에 있지 않은(스태틱 메서드) from, isArray 등의 메서드들은 인스턴스가 직접 호출할 수 없다. 이들은 Array 생성자 함수에서 직접 접근해야 실행이 가능하다.

const arr = [1, 2];
Array.isArray(arr); // (O)
arr.isArry();       // (X) TypeError

 

constructor 프로퍼티

 생성자 함수의 프로퍼티인 prototype 객체 내부에는 constructor라는 프로퍼티가 있다. 인스턴스의 __proto__객체 내부에도 있다. constructor 프로퍼티는 생성자 함수(자기 자신)을 참조한다. 인스턴스로부터 그 원형이 무엇인지를 알 수 있는 수단이 된다.

 한편 constructor는 읽기 전용 속성이 부여된 예외적인 경우(✱기본형 리터럴 변수 - number, string, boolean)을 제외하고는 값을 바꿀 수 있다. 하지만 변경하더라도 참조 대상이 변경될 뿐 이미 만들어진 인스턴스의 원형이 바뀐다거나 데이터 타입이 변하는 것은 아니다.  어떤 인스턴스의 생성자 정보를 알아내기 위해 constructor 프로퍼티에 의존하는 게안전하지는 않은 것이다.

// 모두 생성자 constructor를 가리킨다.
[Constructor]
[instance].__proto__.constructor
[instance].constructor
Object.getPrototypeOf([instance]).constructor
[Constructor].prototype.constructor

// 모두 prototype 객체에 접근
[Constructor].prototype
[instance].__proto__
[instance]
Object.getPrototypeOf([instance])

 


✱추가설명 (기본형 리터럴 변수)

배열을 만드는 방법으로 리터럴과 Array 생성자 함수가 있다. 이는 기본형 데이터인 number, string, boolean도 마찬가지다.

let a = 2;
let b = new Number(2);

let c = 'string'
let d = new String('str');

let e = true;
let f = new Boolean(false);

객체나 배열과는 다르게  리터럴로 생성하는 것과 생성자로 생성하는 것에서 기본형 데이터는 차이를 갖는다.

a는 리터럴로 생성했기 때문에 instance가 아니고, 그렇기 때문에 a instanceof Number는 false다. 그리고 인스턴스 객체가 아니기 때문에 당연히 __proto__속성을 갖지 않는다. typeof로 비교해보았을 때도 리터럴로 생성한 a는 number가 나오지만, Number 생성자로 만든 b는 인스턴스 객체이기 때문에 object가 나온다.

 

 

 


정재남 작가님의〔 코어 자바스크립트 〕책을 공부하며 정리했습니다.

Comments