Programming/javascript

자바스크립트 기초부터 모던 자바스크립트까지 - 프로토타입편

KOOCCI 2022. 7. 30. 16:10

목표 : 자바스크립트의 프로토타입을 설명할 수 있다.


앞서 계속해서 나오던 내용이 프로토타입이다.

__proto__ 접근자 프로퍼티나, prototype 프로퍼티를 다루기도 했고, 이제 프로토타입에 대해 정확히 알아가볼 시간이다.

 

 자바스크립트는 어떤 언어인가?

자바스크립트는 멀티 패러다임 언어다.

다시말해, 명령형, 함수형, 프로토타입 기반 객체지향 프로그래밍을 지원하는 멀티 패러다임 프로그래밍 언어다.

다시 보자. 자바스크립트는 프로토타입을 기반으로 한 객체 지향 프로그래밍 언어다.

자바스크립트는 객체 기반의 프로그래밍 언어로 자바스크립트를 이루고 있는 거의 모든 것이 객체다.

 

결국 다시 객체로 돌아온다.

이제 이 객체로 상속과 프로토타입에 대해 알아보자.

 

상속과 프로토타입

상속어떤 객체의 프로퍼티 또는 메서드를 다른 객체가 상속받아 그대로 사용할 수 있는 것이다.

자바스크립트에서는 상속을 프로토타입 기반으로 구현하여, 코드를 재사용한다.

 

// 생성자 함수
function Circle(radius) {
    this.radius = radius;
    this.getDiameter = function() {
    	return 2 * this.radius;
    };
}

const circle1 = new Circle(5);
const circle2 = new Circle(7);

console.log(circle1.getDiameter); // ƒ () { return 2 * this.radius;}
console.log(circle1.getDiameter); // ƒ () { return 2 * this.radius;}

console.log(circle1.getDiameter === circle2.getDiameter); // false

앞서 생성자 함수에서 살펴보았듯이, 생성자 함수동일한 프로퍼티나 메서드 구조를 갖는 객체를 여러 개 생성할 때 유용하다.

그런데, 위 예시는 좀 아쉬운 것이 있다.

getDiameter 메서드는 단 하나만 생성해서 모든 인스턴스가 공유할 때 가장 바람직해 보인다.

그러나, 생성자 함수로 인스턴스가 생성될 때마다, 중복생성하고 중복 소유하는 형태다.

 

// 생성자 함수
function Circle(radius) {
    this.radius = radius;
}

Circle.prototype.getDiameter = function() {
    return 2 * this.radius;
};

const circle1 = new Circle(5);
const circle2 = new Circle(7);

console.log(circle1.getDiameter); // ƒ () { return 2 * this.radius;}
console.log(circle1.getDiameter); // ƒ () { return 2 * this.radius;}

console.log(circle1.getDiameter === circle2.getDiameter); // true

Circle 생성자 함수에 prototype 이라는 데이터 프로퍼티에 메서드로 getDiameter를 할당하였다.

생성자 함수에 prototype앞서 알아본 바와 같이, 생성자 함수로 호출 할 수 있는 함수 객체. 즉, constructor 만이 소유하는 프로퍼티다.

그리고, 함수가 객체를 생성하는 생성자 함수로 호출될 때, 생성자 함수가 생성할 인스턴스의 프로토타입 객체를 가리킨다.

따라서, Circle 생성자 함수가 생성하는 모든 인스턴스는 getDiameter라는 메서드를 상속받아 사용할 수 있게 된다.

 

프로토타입 객체

프로토타입 객체(= 프로토타입)는 객체 지향 프로그래밍의 근간을 이루는 객체 간 상속을 구현하기 위해 사용된다.

어떤 객체의 상위(부모) 객체의 역할을 하는 객체로서 다른 객체에 공유 프로퍼티(또는 메서드)를 제공한다.

프로토타입을 상속받은 하위 객체는 상위 객체의 프로퍼티를 자신의 프로퍼티처럼 사용할 수 있다.

 

조금 더 세부적으로 들어가보자.

[[Prototype]]이라는 내부 슬롯이 있다.

이 내부 슬롯의 값은 프로토타입 참조(null일수도 있다)다.

[[Prototype]]에 저장되는 프로토타입은 객체 생성 방식에 의해 결정된다.

 

즉, 객체가 생성될 때 객체 생성 방식에 따라 프로토타입이 결정된다. 그리고 그 프로토타입의 참조값은 [[Prototype]]에 저장된다.

 

객체 리터럴로 하나의 객체가 생성될 것이라고 하자.

이 객체의 프로토타입은 객체 생성 방식에 따라, Object.prototype이 된다.

 

생성자 함수에 의해 생성된 객체가 있다고 하자

이 객체의 프로토타입은 객체 생성 방식에 따라, 생성자 함수의 prototype 프로퍼티에 바인딩되어 있는 객체다.

 

모든 객체하나의 프로토타입을 갖는다. 그리고 모든 프로토타입은 생성자 함수와 연결되어 있다.

객체와 생성자함수와 프로토타입의 관계

하나의 객체를 생성하고자 한다.

위 예시에서 사용한 circle1 이라고 하자.

circle1의 생성은 생성자 함수를 통하여 되었다.

circle1(3번)의 프로토타입은 객체 생성 방식에 따라, 생성자 함수 Circle(1번)의 prototype 프로퍼티에 바인딩되어 있는 Circle.prototype 객체(2번)다.

// 생성자 함수
function Circle(radius) {
    this.radius = radius;
}

Circle.prototype.getDiameter = function() {
    return 2 * this.radius;
};

const circle1 = new Circle(5);

console.log('생성자 함수 = [1번]');
console.log(Circle);
// ƒ Circle(radius) {
//     this.radius = radius;
// }

console.log('[1번]의 prototype 프로퍼티 = [2번]');
console.log(Circle.prototype);
// {getDiameter: ƒ, constructor: ƒ}

console.log('[2번]의 constructor 프로퍼티');
console.log(Circle.prototype.constructor);
// ƒ Circle(radius) {
//     this.radius = radius;
// }

console.log('[1번] 과 [2번]의 construcotor 프로퍼티 관계');
console.log(Circle === Circle.prototype.constructor);  
// true

console.log('[1번]으로 생성된 인스턴스 = [3번]');
console.log(circle1);
// Circle {radius: 5}

console.log('[3번]의 __proto__ 접근자 프로퍼티');
console.log(circle1.__proto__);
// {getDiameter: ƒ, constructor: ƒ}

console.log('[3번]의 __proto__ 접근자 프로퍼티와 [2번]의 관계');
console.log(Circle.prototype === circle1.__proto__); 
// true

console.log('[3번]의 constructor');
console.log(circle1.constructor);
// ƒ Circle(radius) {
//     this.radius = radius;
// }

Circle 생성자 함수는 circle1 객체를 생성했다.

이때, circle1 객체는 프로토타입의 constructor 프로퍼티를 통해 생성자 함수와 연결된다.

circle1 객체는 constructor 프로퍼티가 없지만, circle1 객체의 __proto__인 Circle.prototype에는 constructor가 있다.

따라서, 상속받아서 Circle과 연결된다.

 

아래 예시를 보며 한번 정리해보도록 하자.

// obj 객체를 생성한 생성자 함수는 Object이다.
const obj = new Object();
console.log(obj.constructor === Object); // true

// add 함수 객체를 생성한 생성자 함수는 Function이다
const add = new Function('a', 'b', 'return a+b');
console.log(add.constructor === Function); // true

// 생성자 함수
function Person(name) {
  this.name = name;
}

// me 객체를 생성한 생성자 함수는 Person이다.
const me = new Person('Lee');
console.log(me.constructor === Person); // true

 

리터럴 타입으로 생성된 객체에서는 어떻게 될까?

위에서 생성자 함수에 대해 프로토타입이 어떻게 되는지 알아보았다.

그렇다면 리터럴 타입으로 생성된 객체에 대해서는 프로토타입이 어떻게될까?

// 객체 리터럴
const obj = {};

// 함수 리터럴
const add = function(a, b) { return a+b; };

// 배열 리터럴
const arr = [1,2,3];

// 정규 표현식 리터럴
const regex = /ab+c/;

console.log(obj.constructor === Object); // true
console.log(add.constructor === Function); // true
console.log(arr.constructor === Array); // true
console.log(regex.constructor === RegExp); // true

객체 리터럴로 생성한 obj임에도, 그 constructor는 Object가 나왔다.

함수 리터럴은 Function으로 생성하지 않았는데도, Function이 나온다.

 

이에 대해서 ECMA Script 사양에 별도로 예외적으로 적혀있다.

특히, Function은 클로져같은 제약사항이 있는 문법인데도, 함수 리터럴의 생성자라고 나오고 있다.

다만, 함수라는 그 특성 자체는 동일하므로 어느정도 연관성이 있는 것으로 보고 넘어가도록 하자.

 

프로토타입 체인

// 생성자 함수
function Person(name) {
  this.name = name;
}

// 프로토타입 메서드
Person.prototype.sayHello = function() {
  console.log(`Hi My name is ${this.name}`);
};

const me = new Person("James");
console.log(me.hasOwnProperty('name')); // true

위 예시를 보면 알 수 있듯이, 자바스크립트는 객체의 프로퍼티(메서드 포함)에 접근하려고 할 때, 해당 객체에 원하는 프로퍼티가 있는지 찾는다.

없다면, [[Prototype]] 내부 슬롯의 참조를 따라 자신의 부모 프로토타입에서 순차적으로 찾기 시작한다.

이를 프로토타입 체인이라고 한다.

이 체이닝의 종점은 Object.prototype일 것이다.

프로토타입 체인 프로퍼티 검색을 위한 메커니즘이라면, 이전에 공부한 식별자는 스코프 체인으로 검색할 것이다.

 

이 프로토타입 체인을 통해, instanceof 같은 연산자도 사용할 수 있다.

instanceof는 객체의 체이닝 내에 존재하는 프로토타입인지 검색하여, true/false로 평가한다.

 

Wrap up

 

자바스크립트의 프로토타입에 대해 알아보았다.

이는 Class에 대한 내용이랑도 연결되며, 앞서 배웠던 생성자, 프로퍼티 등의 개념이 최종적으로 어우러지는 부분이라는 것을 알 수 있다.

결국 프로토타입객체간의 상속을 표현하기 위해서 사용되었고, 그로인해 오버라이딩과 같은 기능도 동일하게 구현할 수 있었다.

 

이제 다음은 This 객체부터 조금 자세히 알아보자.