제육's 휘발성 코딩
article thumbnail
반응형

프로토타입 vs 클래스

프로그래밍 언어에는 상속을 사용하는 클래스 기반의 언어와 객체를 프로토타입으로 삼고 이를 복제하는 프로토타입 기반 언어가 있습니다.

자바스크립트는 프로토타입 기반의 언어로 만들어져 있습니다. 
 
프로토타입 기반 언어는 객체를 생성할 때 객체를 다른 객체 기반으로 생성합니다.

이를 프로토타입 체인이라고 부르며, 체인 상의 상위 객체를 상속받아 생성하여 상위 객체의 프로퍼티와 메서드를 사용할 수 있습니다. 
 


클래스 기반의 언어는 정형화된 구조를 가지고 있어 코드의 재사용성과 유지보수성이 높지만, 프로토타입 기반의 언어는 프로토타입 체인을 통해 비정형화된 구조를 갖고 있으나, 객체를 유연하게 생성하고 확장할 수 있어 코드의 표현력이 높습니다.

 

프로토타입 사용 목적

프로토타입은 자바에서 static을 사용하는 것처럼 공유 변수를 통한 메모리 최적화를 할 수 있습니다. 

function KoreaPerson(name, age) {
    this.name = name;
    this.age = age;
    this.nationality = 'Korea';
}

const personA = new Person('A', 10);
const personB = new Person('B', 20);

KoreaPerson 객체에 이름과, 나이, 국적을 저장하고, personA와 personB 인스턴스를 생성하였습니다.


personA와 B 모두 같은 한국인을 저장하기 위한 인스턴스인데 불필요하게 nationality가 각각의 인스턴스에 존재하여 메모리를 소모하고 있습니다. 
 

function KoreaPerson(name, age) {
    this.name = name;
    this.age = age;
}

KoreaPerson.prototype.nationality = 'Korea';

const personA = new KoreaPerson('A', 10);
const personB = new KoreaPerson('B', 20);

console.log(personA.nationality);

다음과 같이 프로토타입을 이용해서 공유 변수를 지정해주면, 하나의 공유 변수로 필드를 구성할 수 있습니다.
 

프로토타입 도식

프로토타입의 핵심 개념은 prototype 프로퍼티와 __proto__ 프로퍼티 입니다. 

두 프로퍼티는 각각 객체 생성, 인스턴스 생성 시에 자동으로 생성되며, prototype 객체 내부에는 인스턴스가 사용할 메서드를 저장합니다.

이 prototype을 참조하는 것이 __proto__입니다. __proto__ 프로퍼티의 핵심은 생략 가능한 프로퍼티라는 점입니다. 관련해서 예제에서 다루도록 하겠습니다.


프로토타입 도식

프로토타입을 도식으로 그리자면 위 그림과 같습니다. 

  1. 어떤 Constructor (생성자 함수)를 new 연산자와 함께 호출
  2. Contructor에서 정의된 내용을 바탕으로 새로운 인스턴스 생성 
  3. instance에 __proto__ 라는 프로퍼티가 자동 부여 
  4. __proto__ 프로퍼티는 Constructor의 prototype을 참조 

 


__proto__는 double underscore의 줄임말로 dunder proto라고 불립니다. 

ES5 명세에는 __proto__ 가 아니라 [[prototype]] 으로 명칭돼어 있으며, __proto__와 같이 직접 접근하는 것을 허용하지 않고 Reflect.getPrototypeOf(instance), Object.getPrototypeOf(instance)를 통해서만 접근하도록 정의했습니다. 

하지만, 대부분의 브라우저들이 __proto__ 를 직접 접근하는 방식을 포기하지 않았고, 결국 ES6부터 정식으로 브라우저에 한해서 정식으로 인정하기에 이르렀습니다. 따라서 브라우저 환경이 아닌 다른 환경에서는 __proto__를 사용을 권장하지 않습니다.

 

프로토타입 예제

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

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

 
위의 코드는 Person이라는 생성자 함수에 getName이라는 메서드를 생성하였습니다.


이제 Person을 new를 통해 생성한 인스턴스는 __proto__를 사용하여 getName 메서드를 호출할 수 있습니다. 
 

const suzi = new Person('Suzi');
console.log(suzi.__proto__.getName());              // undefined
console.log(suzi.getName());                        // Suzi
console.log(Person.prototype === suzi.__proto__);   // true

suzi라는 Person 인스턴스를 생성하여 suzi.__proto_.getName()을 통해 __proto__ 프로퍼티가 참조하는 getName() 메서드를 호출했지만 undefined가 출력되고, suzi.getName() 은 잘 출력됩니다. 
 
 
메서드에 대해 다시 되돌아봅시다. 메서드는 객체 안에서 사용되기 위한 함수로 .앞에 까지가 this 스코프가 됩니다.

따라서 suzi.__proto__. getName()을 하게 되면 현재의 this 스코프는 suzi.__proto__가 되겠죠.

suzi.__proto__ 스코프에서 접근할 name 프로퍼티가 존재하지 않기 때문에 undefined가 출력됩니다. 


반대로 suzi.getName()은 현재 this 스코프가 suzi 객체인 상황이고, 이 객체 스코프에선 name을 접근할 수 있기 때문에 원하는 값이 출력됩니다.


인스턴스인 suzi는 __proto__를 통해 prototype을 참조하게 되어있는데, 생략 가능한 프로퍼티이기 때문에 __proto__를 생략해야만 접근이 가능한거죠.
 

suzi.__proto__._name = 'Suzi';
console.log(suzi.__proto__.getName());              // Suzi

다음과 같이 suzi.__proto__ 에서 name 프로퍼티를 지정해 준다면 우리가 원하는 값을 볼 수 있습니다. 
 

크롬 개발자 도구 (객체, 인스턴스 정보)

Constructor 객체와 Constructor의 인스턴스인 instance를 생성하여 크롬 개발자 도구로 확인해 보면 다음과 같이 출력됩니다.

인스턴스의 __proto__는 객체의 prototype을 참조하기 때문에 같은 정보를 갖고 있네요.
 

크롬 개발자 도구 (Array 정보)

arr이라는 배열을 생성해서 디렉토리 구조를 출력해 보면 위의 그림과 같이 나옵니다.

첫 줄에 Array(2)라고 표기된 것은 Array라는 생성자 함수를 원형으로 삼아 생성돼었다는 의미입니다. (프로토타입 언어 방식) 
 

[[prototype]] (__proto__) 에는 Array 객체에서 사용할 수 있는 메서드들이 모두 들어 있습니다.


즉, Array 객체의 prototype을 참조하여 arr 인스턴스의 [[prototype]] 정보에 담기게 됩니다. 
 

constructor 프로퍼티

프로토타입 객체 내부에는 constructor 프로퍼티가 존재합니다. 인스턴스인 __proto__ 내부에도 마찬가지로 constructor 프로퍼티가 존재합니다. 

const arr = [1, 2];
console.log(Array.prototype.constructor === Array); // true
console.log(arr.__proto__.constructor === Array);   // true
console.log(arr.constructor === Array);             // true

 constructor 프로퍼티를 통해 인스턴스로부터 그 원형이 무엇인지를 알 수 있습니다. 
 

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

const p1 = new Person('sasca');
const p1Proto = Object.getPrototypeOf(p1);
const p2 = new Person.prototype.constructor('sasca2');
const p3 = new p1Proto.constructor('sasca3');
const p4 = new p1.__proto__.constructor('sasca4');
const p5 = new p1.constructor('sasca5');

[p1, p2, p3, p4, p5].forEach( p => console.log(p instanceof Person)); // true

constructor를 사용해서 다양하게 객체를 생성할 수 있습니다. 위의 방식 모두 Person 인스턴스가 됩니다. 
 

프로토타입 체인

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

위의 코드를 브라우저에서 출력하면 [[prototype]] : Array 하위에 [[Prototype]] : Object가 존재합니다. 


그 이유는 prototype이 객체 타입이기 때문입니다. 기본적으로 모든 객체의 [[Prototype]]에는 Object.prototype이 연결됩니다.
 

Array 타입의 인스턴스 프로토타입 도식화

위의 arr 인스턴스의 프로토타입을 도식화하면 다음과 같습니다.

__proto__은 생략 가능하기 때문에 arr 인스턴스는 Array.prototype과 Object.prototype의 내부 메서드를 자신의 것처럼 실행할 수 있게 됩니다.  
 
Array 객체뿐만 아니라 Number, String, Boolean 등 다양한 데이터 타입들 모두 동일한 형태의 프로토타입 체인 구조를 갖게 됩니다.
 


프로토타입 체인상 가장 마지막은 Object.prototype이 존재합니다.
다만, 예외적으로 Object.create(null)를 사용한 경우 Object.prototype이 없는 객체를 생성합니다. 이 메서드를 사용하면, 내장 메서드 및 프로퍼티들이 제거됨으로써 제약이 생기지만, 객체의 무게가 가벼워지는 성능상 이점을 가져갈 수 있습니다. 

 

반응형
profile

제육's 휘발성 코딩

@sasca37

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요! 맞구독은 언제나 환영입니다^^