모래블로그

[JavaScript] 프로토타입 본문

Language/JavaScript

[JavaScript] 프로토타입

별모래 2024. 3. 12. 14:12
728x90

 

자바스크립트는 프로토타입 기반 언어라고 불린다.

 

자바스크립트의 모든 객체는 자신의 부모 역할을 담당하는 객체와 연결되어 있다.

그리고 이것은 마치 객체 지향의 상속 개념과 같이, 부모 객체의 프로퍼티 또는 메소드를 상속받아 사용할 수 있게 하는데

이러한 부모 객체를 Prototype(프로토타입) 객체 또는 줄여서 Prototype(프로토타입)이라 부른다.

 

Prototype 객체는 생성자 함수에 의해 생성된 각각의 객체에 공유 프로퍼티를 제공하기 위해 사용한다.

 

 

Prototype vs Class

자바스크립트에는 클래스라는 개념이 없지만, 프로토타입(prototype) 이라는 것이 존재한다.

자바스크립트가 프로토타입 기반 언어라고 불리는 이유이다.

 

클래스가 없기 때문에 기본적으로 상속 기능도 없다. 그래서 보통 프로토타입을 기반으로 상속을 흉내내도록 구현하여 사용한다.

 

그러나 최근 ECMA6 표준에서는 Class 문법이 추가 되었다. 하지만 추가 되었을 뿐이지, 자바스크립트가 클래스 기반으로 바뀌었다는 것은 아니다.


 

 

자바스크립트에서는 객체의 prototype(객체 멤버인 __proto__ 속성으로 접근 가능한) 과 생성자의 prototype 속성의 차이를 알아야하는데,

전자는 '개별 객체의 속성'이며, 후자는 '생성자의 속성' 이다.

 

예를 들어, Object.getPrototypeOf(new Test())의 return 값은 Test.prototype와 동일한 객체이다.

전자와 후자의 차이점을 정확하게 이해하려면 JS의 함수의 구조와 객체 내부의 구조부터 정확하게 알고 있어야한다.


1.  함수와 객체의 내부구조

자바스크립트에서는 함수를 정의하고, 파싱단계에 들어가면 내부적으로 수행되는 작업이 있다.

함수 멤버로는 prototype 속성이 있고, 이 속성은 다른 곳에 생성된 함수 이름의 프로토타입 객체를 참조한다.

프로토타입 객체의 멤버인 constructor 속성은 함수를 참조하는 내부구조를 가진다.

 

 

function Person() { } // Person의 prototype 속성은 프로토타입 객체를 참조

// 생성된 모든 객체는 Person prototype 객체를 참조
var A = new Person();
var B = new Person();

① 속성이 아무것도 없는 Person 이라는 함수가 정의되고, 파싱단계에서 Person 함수의 Prototype 속성은 프로토타입 객체를 참조한다.

② Person Prototype 객체 멤버인 constructor 속성은 Person 함수를 참조하는 구조이다.

Person Prototype 객체 new 라는 연산자와 Person 함수를 통해서 생성된 모든 객체의 원형이 되는 객체이다. (생성된 모든 객체가 참조한다.)

 

 

자바스크립트에서는 기본 데이터 타입인 boolean, number, string, null, undefined 를 제외하고는 모두 객체이다.

사용자가 정의한 함수나, new 연산자를 통해 생성된 것도 객체이다.

이러한 객체 안에는 기본적으로 proto 속성이 있는데, 이 속성은 객체가 만들어지기 위해 사용된 원형 프로토타입 객체(그림에서 Person Prototype 객체)를 숨은 링크로 참조하는 역할을 한다.

 

 

2.  프로토타입 객체

함수를 정의 했을 때 다른 곳에 생성되는 프로토타입 객체는 프로토타입 객체 자신이 다른 객체의 원형이 되는 객체이다.

모든 객체는 프로토타입 객체에 접근할 수 있고, 프로토타입 객체도 동적으로 런타임에 멤버를 추가할 수 있다.

같은 프로토타입을 원형으로 해서 복사된(생성된) 모든 객체는 추가된 멤버를 사용할 수 있다.

 

[예제 1]

function Person () {} // Person의 prototype 속성은 프로토타입 객체를 참조

// 생성된 모든 객체는 Person prototype 객체를 참조
var A = new Person();
var B = new Person();

// 프로토타입 객체에서 동적으로 멤버(getType) 추가
Person.prototype.getType = function () {
	return "사람";
};

// 원형을 복사로 생성된 객체는 추가된 멤버(getType) 사용가능
console.log(A.getType()); // 사람
console.log(B.getType()); // 사람

단, prototype 객체에 멤버를 추가/수정/삭제 할 때는 함수 안의 prototype 속성을 사용해야 한다.

하지만 prototype 멤버를 읽을 때는 함수 안의 prototype 속성 또는 객체 이름으로 접근한다.


[예제 2]

function Person () {} // Person의 prototype 속성은 프로토타입 객체를 참조

// 생성된 모든 객체는 Person prototype 객체를 참조
var A = new Person();
var B = new Person();

// 프로토타입 객체에서 동적으로 멤버(getType) 추가
Person.prototype.getType = function () {
	return "사람";
};

A.getType = function() {
	return "인간";
}

console.log(A.getType()); // 인간
console.log(B.getType()); // 사람

B.age = 27;

console.log(A.age); // undefined
console.log(B.age); // 27

Person prototype 객체를 원형으로 하는 A 객체에서 getType 멤버를 수정 하였는데, 

이런 경우 원형인 prototype 객체에 있는 멤버를 수정하는 것이 아니라 자신의 객체에 멤버를 추가하는 것이다.

결국, A 객체를 사용하여 getType()를 호출하면 프로토타입 객체의 getType()을 호출한 것이 아니라, A 객체에 추가된 getType()를 호출한 것이다.

 

프로토타입 객체의 멤버를 수정하고 싶은 경우, 멤버 추가와 같이 함수의 prototype 속성을 이용하여 수정해야 한다.

Person.prototype.getType = function() {
	return "인간";
}
	
console.log(B.getType()); // 인간

3. 프로토타입이란 ?

자바스크립트에서 기본 데이터 타입을 제외한 모든 것이 객체이다.

객체가 만들어지기 위해서는 자신을 만드는데 사용된 원형인 prototype 객체를 이용하여 객체를 만든다.

이 때 만들어진 객체 안에 __proto__ 라는 속성(멤버)자신을 만들어낸 원형을 의미하는 프로토타입 객체를 참조하는 숨겨진 링크가 있다. 이 숨겨진 링크프로토타입이라고 정의한다.

 

자바스크립트에서의 프로토타입은 크게 두 가지로 해석이 된다.

Person 함수의 멤버인 prototype 속성이 Person prototype 객체를 참조하는 속성이다. 그리고 함수와 new 연산자가 만나 생성된 객체의 prototype 객체를 지정해주는 역할을 한다.

생성된 객체 안의 __proto__ 속성은 자신을 만들어낸 원형인 Person pototype 객체를 참조하는 숨겨진 링크인데, 이 링크로 Person prototype 객체 멤버에 접근하는 용도로서의 프로토타입을 의미한다.

 

 

4. 코드의 재사용

프로토타입을 이용하여 코드 재사용을 할 수 있다.

 

이 방법에도 크게 두 가지로 분류할 수 있는데,  classical 방식prototypal 방식이 있다.

두 가지 방법 중 자바스크립트에서는 prototypal 방식을 더 선호하는데, 그 이유는 classical 방식보다 간결하게 구현할 수 있기 때문이다.

 

1) classical 방식

new 연산자를 통해 생성한 객체를 사용하여 코드를 재사용 하는 방법이다. 

마치 Java에서 객체를 생성하는 방법과 유사하여 classical 방식이라고 한다.

 

 

[예제1] 기본 방법

 

부모에 해당하는 함수를 이용하여 객체를 생성한다.

자식에 해당하는 함수의 prototype 속성을 부모 함수를 이용하여 생성한 객체를 참조하는 방법이다.

// 부모 함수
function Person(name) {
    this.name = name || "A";   // name = "A";   
}

// Person 프로토타입 객체에 getName 속성 추가
Person.prototype.getName = function () {
    return this.name;
}

// 자식 함수
function Korean(name){}

// 자식 함수의 프로토타입 객체의 속성을 부모 프로토타입 객체의 속성으로 변경
Korean.prototype = new Person();

var person1 = new Korean();
var person2 = new Korean("B");

console.log(person1.getName());  // A
console.log(person2.getName());  // A

부모에 해당하는 함수는 Person 이다.

Korean.prototype = new Person(); 을 통해 자식 함수인 Korean 함수 안의 프로토타입 속성을 부모 함수로 생성된 객체로 바꿨다.

이제 Korean 함수와 new 연산자를 이용하여 생성된 person 객체의 _proto__ 속성이 부모 함수를 이용하여 생성된 객체를 참조한다.

이 객체가 Korean 함수를 이용하여 생성된 모든 객체의 프로토타입 객체가 된다.

person1에는 name과 getName() 이라는 속성은 없지만, 부모에 해당하는 프로토타입 객체에 name 이 있다. 이 프로토타입 객체의 부모에 getName()을 가지고 있어 person1 에서 사용할 수 있다.

이 방법에도 단점이 있는데, 부모 객체의 속성과 부모 객체의 프로토타입 속성을 모두 물려받게 된다. 대부분의 경우 객체 자신의 속성은 특성 인스턴스에 한정되어 재사용할 수 없어 필요가 없다. 또한, 자식 객체를 생성할 때 인자를 넘겨도 부모 객체를 생성할 때 인자를 넘겨주지 못한다.

person2 객체를 생성할 때, Korean 함수의 인자로 B라고 주었다. 객체를 생성한 후 getName()을 호출하면 B라고 출력될 것 같지만, 부모 생성자에 인자를 넘겨주지 않았기 때문에 name 에는 default 값인 A 가 들어있다.

객체를 생성할 때마다 부모의 함수를 호출할 수도 있지만 매우 비효율적이다.

 

[예제2] 생성자 빌려쓰기

 이 방법은 기본 방법의 문제점인 자식 함수에서 받은 인자를 부모 함수로 인자를 전달하지 못했던 부분을 해결한다.

부모 함수의 this에 자식 객체를 바인딩하는 방식이다.

unction Person(name) {
    this.name = name || "A"; // name = "A"
}

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

function Korean(name) {
    Person.apply(this, arguments);
}

var person1 = new Korean("B");
console.log(person1.name); // B

Korean 함수를 정의하는 부분을 보면 함수 내부에서 apply 함수를 이용한다. 이를 통해 부모 객체인 Person 함수의 this 를 Korean 함수 안의 this 로 바인딩한다. (new 연산자로 Korean 함수의 "B"로 파라미터를 줄 때, Person 함수의 this.name에 바인딩 해 Korean의 파라미터를 arguments의 유사배열로 받아서 Person 함수를 실행한다.

부모의 속성을 자식 함수 안에 모두 복사하며, 객체를 생성하고 name을 출력한다. 객체를 생성할 때 넘겨준 인자를 출력하는 것을 볼 수 있다.

기본방법에서는 부모객체의 멤버를 참조를 통해 물려받았지만, 생성자 빌려 쓰기는 부모객체 멤버를 복사하여 자신의 것으로 만들어버리기 때문에 이 방법은 부모객체의 this로 된 멤버들만 물려받게 되는 단점이 있다. 그래서 부모 객체의 프로토타입 객체의 멤버들을 물려받지 못한다. 위 그림에서 person1 객체에서 부모 객체의 프로토타입인 Person Prototype 객체의 링크가 없다는 것을 볼 수 있다.

 

 

[예제3] 생성자 빌려 쓰고 프로토타입 지정해주기

이 방법은 예제 1, 2 방법의 문제점들을 보완하면서 Java 에서 예상할 수 있는 동작 방식과 유사하다.

function Person(name) {
    this.name = name || "A"; // name = "A"
}

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

function Korean(name) {
    Person.apply(this, arguments);
}
Korean.prototype = new Person();

var person1 = new Korean("B");
console.log(person1.getName()); // B

 

Korean 함수 하단에 Korean 함수의 prototype 객체를 Person 함수를 사용하여 생성된 객체로 지정하는데, 

부모(Person) 객체 속성(this.name)에 대한 참조를 가지는 것이 아닌 복사본을 통해 내 것으로 만듦과 동시에 부모 객체의 prototype 객체에 대한 링크도 참조된다. 부모 객체의 Prototype 객체 멤버도 사용할 수 있다.

차이점은 person1 객체에 name 멤버를 가지고 있다는 점이다. 그러나 이 방법에도 문제점이 있는데, 바로 부모 생성자를 2번 호출한다는 점이다. 생성자 빌려쓰기 방법과는 달리 getName()은 제대로 상속되었지만, name에 대해서는 person1 객체와 부모 함수(Person)를 이용하여 생성한 객체(new Person())에도 name이 있는 것을 볼 수 있다.

 

 

[예제4] 프로토타입 공유

이 방법은 부모 생성자를 한 번도 호출하지 않으면서 프로토타입 객체를 공유하는 방법이다.

function Person(name) {
    this.name = name || "A"; 		// name = "A"
}

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

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

var person1 = new Korean("B");
console.log(person1.getName()); // B

자식 함수(Korean)의 prototype 속성을 부모 함수(Person)의 prototype 속성이 참조하는 객체로 설정했다.

자식 함수를 통해 생성된 객체는 부모 함수를 통해 생성된 객체(new Person())를 거치지 않고 부모 함수의 Prototype 객체를 부모로 지정하여 객체를 생성한다. 부모 함수의 내용은 상속받지 못하기 때문에 상속받으려는 부분을 부모 함수의 Prototype 객체에 작성해야 원하는 결과를 얻을 수 있다.


2) prototypal 방식

리터럴 또는 Object.create()를 이용하여 객체를 생성함과 동시에 Prototype 객체를 지정한다.

첫 번째 매개변수는 부모 객체로 사용할 객체를 넘겨주고, 두 번째 매개변수는 선택적 매개변수로 자식 객체의 속성에 추가되는 부분을 넣어준다.

 

var person = {
    type : "사람",
    getType : function() {
        return this.type;
    },
    getName : function() {
        return this.name;
    }
};

var a = Object.create(person);
a.name = "A";

console.log(a.getType());  // 사람
console.log(a.getName());  // A

 

부모 객체인 person 은 객체 리터럴 방식으로 객체를 생성하고, 자식 객체인 a 는 Object.create() 함수를 통해 첫 번째 매개변수로 person을 넘겨 받아 객체를 생성했다.

단 한줄로 객체를 생성함과 동시에 부모 객체인 person 객체의 속성도 물려 받았다.

자바스크립트에서는 new 연산자와 함수를 통해 생성한 객체를 사용하는 classical 방식 보다 prototypal 방식을 더 선호한다.

 

 

 


참조

https://poiemaweb.com/js-prototype

https://medium.com/@bluesh55/javascript-prototype-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0-f8e67c286b67

https://spiderwebcoding.tistory.com/10

https://velog.io/@pds0309/Javascript-Prototype-%EA%B8%B0%EB%B3%B8-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0#javascript-%ED%94%84%EB%A1%9C%ED%86%A0%ED%83%80%EC%9E%85

 

728x90

'Language > JavaScript' 카테고리의 다른 글

[JavaScript] 클로저(Closure)  (0) 2024.03.14
[JavaScript] ECMAScript와 JavaScript  (0) 2024.02.19
[JavaScript] Generator(제너레이터)  (0) 2024.02.16
[JavaScript] 동기, 비동기  (0) 2024.02.15
[JavaScript] 구조 분해 할당  (0) 2024.02.14