[JS] DeepDive(19) 프로토타입 - 1
FrontEnd/Deep Dive

[JS] DeepDive(19) 프로토타입 - 1

728x90

자바스크립트는 명령형, 함수형, 프로토타입 기반 객체지향 프로그래밍을 지원하는 멀티 패러다임 프로그래밍 언어이다.

 

물론 JS는 클래스기반 객체지향 프로그래망과는 다르게 클래스,상속,public,private,protected 등이 없다. 하지만 JS는 클래스 기반 객체지향 프로그래밍 언어보다 효율적이며 더 강력한 객체지향 프로그래밍 능력을 지닌 프로토타입 기반의 객체지향 프로그래밍 언어이다.

 

 

물론 ES6에서 클래스가 도입되긴 했다. JS에선 클래스도 함수이며 기존 프로토타입 기반 패턴의 문법적 설탕이다. 

 

JS는 객체기반의 프로그래밍 언어이며 JS를 이루는 거의 모든 것이 객체이다.

 

 

객체지향 프로그래밍

객체지향 프로그래밍은 프로그램을 명령어 또는 함수의 목록으로 보는 전통적인 명령형 프로그래밍의 절차지향적 관점에서 벗어나 여러 개의 독립적 단위, 즉 객체의 집합으로 프로그램을 표현하려는 프로그래밍 패러다임이다.

 

객체지향 프로그래밍은 실세계의 실체를 인식하는 철학적 사고를 프로그래밍에 접촉하려는 시도에서 시작하였다.

 

이러한 방식을 프로그래밍에 접목시켜 필요한 속성만 간추려 표현하는 것을 추상화라고 한다.

 

 

const person = {
	name : 'Jeong',
    address : 'Incheon',
};

 

이때 프로그래머는 이름과 주소 속성으로 표현된 객체인 person을 다른 객체와 구분하여 인식할 수 있다.

 

 

 

상속과 프로토타입

 

 

상속은 객체지향 프로그래밍의 핵심 개념으로 어 떤 객체의 프로퍼티 또는 메서드를 다른 객체가 상속받아 그대로 사용할 수 있는 것을 말한다.

 

 

 

 

JS는 프로토타입을 기반으로 상속을 구현하여 불필요한 중복을 제거한다.

 

 

 

// 생성자 함수
function Circle(radius) {
  this.radius = radius;
  this.getArea = function () {
    // Math.PI는 원주율을 나타내는 상수다.
    return Math.PI * this.radius ** 2;
  };
}

// 반지름이 1인 인스턴스 생성
const circle1 = new Circle(1);
// 반지름이 2인 인스턴스 생성
const circle2 = new Circle(2);

// Circle 생성자 함수는 인스턴스를 생성할 때마다 동일한 동작을 하는
// getArea 메서드를 중복 생성하고 모든 인스턴스가 중복 소유한다.
// getArea 메서드는 하나만 생성하여 모든 인스턴스가 공유해서 사용하는 것이 바람직하다.
console.log(circle1.getArea === circle2.getArea); // false

console.log(circle1.getArea()); // 3.141592653589793
console.log(circle2.getArea()); // 12.566370614359172

위 예제에서 생성자로 동일한  프로퍼티 구조를 가지는 객체를 만들 수 있다. 단 getArea메서드와 같이 동일한 내용조차 중복생성되는 문제가 있다.

 

 

프로토타입을 활용하면 불필요한 중복을 제거할 수 있다.

 

 

function Circle(radius) {
    this.radius = radius;
}
// 생성자 함수에 prototype 프로퍼티에 해당 내용들이 들어가게된다.
// 프로토타입은 Circle 생성자 함수의 prototype 프로퍼티에 바인딩 되어있다.
Circle.prototype.getArea = function () {
    return Math.PI * this.radius ** 2;
};
//인스턴스 생성
const circle1 = new Circle(1);
const circle2 = new Circle(2);

// Circle 생성자 함수가 생성하는 모든 인스턴스는 하나의 getArea 메서드를 공유한다.
console.log(circle1.getArea === circle2.getArea) // true

console.log(circle1.getArea()); // 3.14..
console.log(circle2.getArea()); // 12.56..

 

 

위 예제처럼 객체를 생성하면 getArea메서드가 공유된다.

 

 

상속은 코드의 재사용 관점에서 매우 유용하며 생성자 함수가 생성할 모든 인스턴스는 별도의 구현없이 프로토타입의 자산을 공유하여 사용할 수 있다.

 

 

프로토타입 객체

 

프로토타입 객체란 객체지향 프로그래밍의 근간을 이루는 객체 간 상속을 구현하기 위해서 사용된다. 프로토타입은 어떤 객체의 부모 역할을 하는 객체이다.

 

모든 객체는 [[Prototype]]이라는 내부 슬롯을 가진다.  객체의 종류에 따라 프로토타입이 결정되고 [[Prototype]]에 저장된다.

 

 

 

 

__proto__ 접근자 프로퍼티

 

이전 글에서 잠깐 소개했지만 위 접근자 프로퍼티를 활용하면 [[Prototype]] 내부슬롯에 간접 접근이 가능하다.

 

내부슬롯은 프로퍼티가 아니기 때문에 JS는 우너칙적으로 내부 슬롯과 내부 메서드에 직접 접근하는것을 막는다. 

 

 

__proto__는 getter/setter 함수라고 부르는 접근자 함수를 통해 프로토타입을 취득하거나 할당한다.

 

 

또한 __proto__는 객체가 직접 소유하는 프로퍼티가 아니며 Object.prototype의 프로퍼티이다. 모든 객체는 상속을 통해 Object.prototype.__proto__ 접근자 프로퍼티를 사용할 수 있다.

 

 

const person = { name: 'mingyu' }

console.log(person.hasOwnProperty('__proto__')) //false

console.log(Obect.getOwnPropertyDescriptor(Object.property, '__proto'))
// {get:f, set:f, enumerable: true, configurable: true}


console.log({}.__proto === Object.protoptye) // true

 

 

 

그렇다면 왜 __proto__ 접근자 프로퍼티를 통해 프로토타입에 접근할까? 그 이유는 상호 참조에 의해 프로토타입 체인이 생성되는것을 방지하기 위함이다.

 

const person = {}
const child = {}

child.__proto__ = parent
parent.__proto__ = child // TypeError: Cyclic __proto__ value

접근자 프로퍼티를 활용하면 위와같이 서로가 서로의 프로토타입이 되는 비정상 체인을 방지할 수 있다.

 

 

단 코드 내에서 __proto__ 접근자 프로퍼티를 직접 사용하는 것은 권장되지 않는데, 모든 객체가 해당 프로퍼티를 사용할 수 있는건 아니기 때문이다.

 

// ojb는 프로토타입 체인의 종점이다. 따라서 Object.__proto__를 상속 받을 수 없다.
const obj = Obect.create(null);

// obj는 Object.__proto__를 상속받을 수 없다.
console.log(obj.__proto__) // undefined

// 따라서 __proto__보다 Object.getPrototypeOf 메서드를 사용하는 편이 좋다.
console.log(Object.getPropertyOf(obj)) //null

 

 

 

 

함수 객체의 prototype 프로퍼티

 

함수 객체만이 소유하는 prototype 프로퍼티는 생성자 함수가 생성할 인스턴스의 프로토타입을 가리킨다.

 

(function() {}).hasOwnProperty('prototype') // true

({}).hasOwnProperty('prototype') // false

 

 

prototype 프로퍼티는 생성자함수가 생성할 객체의 프로토타입을 가리키며 non-constructor인 화살표 함수와 ES6메서드 축약 표현으로 정의한 메서드는 prototype 프로퍼티를 소유하지 않는다.

 

 

모든 객체가 가지고 있는 __proto__ 접근자 프로퍼티와 함수객체만이 가지고 있는 prototype 프로퍼티는 결국 동일한 프로토타입을 가리키지만 이를 사용하는 주체가 다르다.

 

 

 

 

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

const me = new Person('mingyu');

console.log(Person.prototype === me.__proto__) // true

 

 

 

 

프로토타입의 constructor 프로퍼티와 생성자 함수

 

모든 프로토타입은 constructor 프로퍼티를 가지며 prototype 프로퍼티로 자신을참조하고 있는 생성자 함수를 가리킨다.

 

 

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

const me = new Person("mingyu");

console.log(me.constructor === Person); //true

 

 

 

 

리터럴 표기법에 의해 생성된 객체의 생성자 함수와 프로토타입

 

리터럴 표기법에 의한 객체 생성 방식과 같이 명시적으로 new 연산자 없이 객체를 생성하는 방식이 있다.

 

이런 객체도 프로토타입이 존재한다. 단 이런 경우 프로토타입의 constructor 프로퍼티가 가리키는 생성자 함수가 반드시 객체를 생성한 생성자 함수라고 단정할 수 없다.

 

객체 리터럴이 평가되느 경우에는 OrdinaryobjectCreate를 호출하여 빈 객체를 생성하고 프로퍼티를 추가하도록 정의되어 있다.

 

즉 Object 생성자 함수 호출과 객체 리터럴의 평가는 추상연산 OrdinaryObjectCreate를 호출하여 빈 객체를 생성하는 점에서는 동일하지만 new.target의 확인이나 프로퍼티를 추가하는 세부 내용이 다르다.

 

따라서 객체 리터럴에 의해 생성된 객체는 Object 생성자 함수가 생성한 객체가 아니다.

 

 

물론 리터럴 표기법에 의해 생성된 객체도 상속을 위해 프로토타입이 필요하다. 해당 객체도 가상적인 생성자 함수를 가진다. 프로토타입은 생성자 함수와 더불어 생성되며 prototype,constructor 프로퍼티에 의해 연결되어 있기 때문이다.

 

 

즉, 프로토타입과 생성자 함수는 단독으로 존재할 수 없으며 언제나 쌍으로 존재하게 된다.

 

 

 

프로토 타입의 생성 시점

 

프로토타입은 생성자 함수가 생성되는 시점에 더불어 생성된다.

 

생성자 함수로서 호출할 수 있는 함수 즉 constructor는 함수 정의가 평가되어 함수 객체를 생성하는 시점에 프로토타입도 더불어 생성된다.

 

console.log(Person.prototype) // {constructor : f}

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

 

생성자 함수로서 호출할 수 없는 함수 즉 non-constructor는 프로토타입이 생성되지 않는다.

 

const Person = name => {
	this.name = name;
}

console.log(Person.prototype);

 

 

 

 

Object,String,number,Function,Array,RegExp,Date,Promise등과 같은 빌트인 생성자 함수도 빌트인 생성자 함수가 생성되는 시점에 프로토타입이 생성된다.

 

모든 빌트인 생성자 함수는 전역 객체가 생성되는 시점에 생성되게 된다.

 

 

 

 

 

이처럼 객체가 생성되기 이전에 생성자 함수와 프로토타입은 이미 객체화되어 존재하며 생성자함수 또는 리터럴 표기법으로 객체를 생성하면 프로토타입은 생성된 객체의 [[Prototype]] 내부 슬롯에 할당되게 된다.

 

 

 

 

 

 

728x90