Prototype 이란

해당 객체의 인스턴스가 가져야할 프로퍼티/메서드를 모든 인스턴스 전체에서 공유하는 것.

1
2
3
4
5
6
7
8
9
10
11
function Person() {}
Person.prototype.name = "Hi";
Person.prototype.sayName = function() {
alert(this.name);
}

var p1 = new Person();
p1.sayName();
var p2 = new Person();
p2.sayName();
console.log(p1.sayName == p2.sayName); // true

Prototype은 어떻게 동작하는가?

함수 생성시

  1. 함수 생성시마다 Prototype property 생성
  2. 자동으로 constructor 프로퍼티 가짐
  3. 각종 프로퍼티와 메서드가 프로토 타입에 추가된다.

constructor에는 해당 프로토타입을 생성한 생성자가 들어있다.

인스턴스 생성시

생성자의 프로토타입을 가리키는 포인터 생성(\proto__)

\proto__ 는 프로토타입이 들어있고, 생성자 자체를 찾아가려면 프로토타입의 constructor를 조회하면 된다.

img

프로토타입의 변수 검색

  1. 해당 인스턴스 내의 변수를 검색한다.
  2. 없으면 \proto__를 통해 프로토타입 내의 변수를 검색한다.

프로토타입의 프로퍼티들은 공유 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Person() {}
Person.prototype.name = "Hi";
Person.prototype.sayName = function() {
alert(this.name);
}

var p1 = new Person();
var p2 = new Person();
p1.name // Hi
p2.name // Hi

Person.prototype.name = "kk";
p1.name // kk

p1.name = 'lala'; // 공유가 끊긴다.

프로토타입을 수정하면, 인스턴스의 변수들 또한 수정되는 것을 확인할 수 있다.

객체 인스턴스에서는 프로토타입 수정 불가

p1.name을 바꿔도 prototype은 수정되지 않는다. p1.name은 프로토타입과 연결이 되어있었지만, 새로운 값을 할당하면서 이제 둘 사이의 연결이 끊긴다. 이 연결은 p1.name = null;으로 해도 다시 돌아오지 않는다. delete p1.name;을 해야 다시 접근이 가능하다.

Prototype의 객체 literal 선언

1
2
3
4
5
6
7
function Person() {}
Person.prototype = {
name: "junho",
sayName : function() {}
}
var friend = new Person();
friend.consturctor == Person // false

위와 같이 프로토타입을 초기화할 수도 있다.

그러나, constructor값이 Object가 되어 버린다. Person으로 되야하는데..

1
2
3
4
5
6
7
8
9
Person.prototype = {
constructor : Person
name: "junho",
sayName : function() {},
}
// 그러나 이러면 constructor의 [[Enumerable]] 속성이 true로 지정됌

// defineProperty를 활용하자.
Object.defineProperty(Person.prototype, "constructor", { enumerable: false, value: Person });

그리고 위에서 설명했듯이 인스턴스와 프로토타입은 공유되고 있어, 프로토타입이 바뀌면 즉시 인스턴스에도 반영이 되지만.

리터럴을 사용하면, 이전 인스턴스들과의 공유가 끊어진다!

Person.prototype = { ... } 을 통해서 프로토타입을 덮어 쒸우면, 새로운 prototype이 생성되서 이전에 생성한 인스턴스와의 공유가 끊어진다.

하지만, 프로토타입을 사용하면 프로퍼티들이 무조건 공유되는데 공유하기 싫을 때는 어떻게 해야하나? 생성자 패턴과 합쳐버리면 된다.

생성자 패턴 + 프로토타입패턴

  1. 생성자 패턴으로 비공유 인스턴스 프로퍼티를 만든다.
  2. 프로토타입으로 메서드 + 공유프로퍼티 정의한다.
1
2
3
4
5
6
7
8
9
10
11
12
function Person(name) {
this.name = name;
this.friends = ["Sunhak", "Jiwon", "JungHyun"];
}
Person.prototype = {
constructor : Person,
sayName : function() {},
}

var p1 = new Person("junho");
var p1 = new Person("sunhak");
// name과 friends는 공유되지 않는다.

그런데 좀 귀찮다… 모든 정보를 생성자 내부에서 초기화해뻐릴 수 없을까?

종속적 프로토타입 패턴

모든 정보를 생성자 내부에서 초기화해보자!

1
2
3
4
5
6
7
8
function Person(name) {
this.name = name;
this.friends = ["Sunhak", "Jiwon", "JungHyun"];

if(typeof this.sayName != "function") {
Person.prototype.sayName : function() {},
}
}

Person.prototype.sayName 은 생성자가 첫번째로 호출된 다음에만 실행된다. if문을 프로퍼티마다 만들 필요 없이 한 if문에 모든 prototype 초기화를 해주면 된다.

방탄 생성자 패턴

this, new 사용금지 환경에서 사용, private의 get/set함수와 비슷하다.

1
2
3
4
5
6
7
function Person(name) {
var o = new Object();
o.sayName = function() { alert(name) }
return o;
}
var friend = Person("hi");
friend.sayName(); // hi

상속

본격적으로 prototype을 이용하여 상속을 받아보자

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function SuperType() {
this.property = true;
}

SuperType.prototype.getSuperValue = function() {
return this.property;
}

function SubType() {
this.subproperty = false;
}

SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function() {
return this.subproperty;
}

var instance = new SubType();
alert(instance.getSuperValue());

instance instanceof Object // true
instance instanceof SuperType // true
instance instanceof SubType // true

Object.prototype.isPrototypeOf(instance); // true
SuperType.prototype.isPrototypeOf(instance); // true
SubType.prototype.isPrototypeOf(instance); // true

img

변수 검색

위에서 말한것처럼 property 검색시 인스턴스 -> SubType -> SuperType 순서대로 프로퍼티를 검색해 나간다. 상위 프로토타입으로 올라갈때는 프로토타입체인 __proto__ 를 이용한다.

다시 또 문제점이 있다. 상속받으면 공유된다. 위의 프로토타입 패턴에서 처럼, 공유하고 싶지 않은 변수들을 어떻게 처리해야할지 생각해봐야 한다.

생성자 훔치기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function SuperType(name) {
this.colors = ["red", "blue"];
this.name = name;
}

function SubType() {
SuperType.call(this, "junho"); // 이 코드가 핵심!
}

var instance = new SubType();
instance.colors.push("black");
console.log(instance.colors); // red blue black
var instance2 = new SubType();
instance2.colors.push("green");
console.log(instance.colors); // red blue green

SuperType 생성자를 새로 생성한 SubType 인스턴스 컨텍스트에서 호출하기 때문에 자신만의 colors 프로퍼티를 갖게 된다.

문제는… 공유해도 되는 함수의 경우 다시 새롭게 생성이 돼서 메모리 낭비가 생긴다는 거다. 공유할 프로퍼티와 그렇지 않을 프로퍼티를 구별할 수 있을까?

조합 상속

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function SuperType(name) {
this.colors = ["red", "blue"];
this.name = name;
}

SuperType.prototype.sayName = function() {
alert(this.name);
}

function SubType(name, age) {
SuperType.call(this, name); // 공유하고 싶지 않은 코드
this.age = age;
}

SubType.prototype = new SuperType();
SubType.prototype.sayAge = function() { // 공유할 함수
return this.age;
}

var instance = new SubType("Junho", 28);
alert(instance.getSuperValue());

자신만의 고유 프로퍼티와 메서드를 공유할 수 있다.

근데 상속을 위한 생성자 함수를 좀 심플하게 만들 수는 없을까?

프로토타입 상속

생성자 함수를 쓰지 않고 상속을 구현해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function object(o) {
function F() {}
F.prototype = o;
return new F();
}

var person = {
name : "hi",
friends : ["junho", "sunhak"]
}

var anotherPerson = object(person);
anotherPerson.name = "hi";
anotherPerson.friends.push("bil");
var anotherPerson2 = object(person);
anotherPerson2.name = "ho";
anotherPerson2.friends.push("ho");

anotherPerson // ["junho", "sunhak", "bil", "ho"]
anotherPerson2 // ["junho", "sunhak", "bil", "ho"]

object 함수에서 자체적으로 생성자 함수를 만들고 prototype에 할당 후 리턴해준다.

ES5에는 위와 같은 패턴을 Object.create 를 통해서 적용할 수 있다.

Object.create(프로토타입이 될 객체, 추가할 프로퍼티 객체)

1
2
3
4
5
6
7
var person = { name: "hi", friends : ["hi", "ho"] }
// p1, p2는 person 객체를 공유하게 된다.
var p1 = Object.create(person);
var p2 = Object.create(person);

var p3 = Object.create(person, { name : { value : "Gre"} })
// 두번째 파라미터는 Object.defineProperties와 유사하다.

기생 상속

객체를 확장해서 반환

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 // object 함수 생략
function createAnother(original) {
var clone = object(original);
clone.sayHi = function() {
alert('hi');
};
return clone;
}

var person = {
name: "junho",
friends: ["She", "junho"]
};

var anotherPerson = createAnother(person);
anotherPerson.sayHi();

anotherPerson객체는 person의 프로퍼티와 메서드를 상속하며 sayHi() 메서드를 추가로 가진다.

기생 조합 상속

조합상속은 JS에서 자주 쓰이지만, 비효율 적인 면이 있음(상위 타입 생성자가 항상 두번 호출됌-? 무슨 말이지)

  1. 하위 타입의 프로토타입을 생성하기 위해
  2. 하위 타입 생성자 내부에서.

하위 타입의 프로토타입은 상위 타입 객체의 인스턴스 프로퍼티를 모두 상속, 하위 타입 생성자 실행시 모두 덮어씀. 두번 실행 별의미가 없음.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function SuperType(name) {
this.name = name;
this.colors = ["red", "blue", "green"];
}

SuperType.protoype.sayName = function() {
alert(this.name);
}

function SubType(name, age) {
SuperType.call(this, name); // SuperType 두번째 호출
this.age = age;
}

SuperType.protoype = new SuperType(); // 처음 호출
SuperType.protoype.consturctor = SubType;
SubType.prototype.sayAge = function() {
alert(this.age);
};

name과 colors프로퍼티는 인스턴스에도 존재, SubType 프로토타입에도 존재. 하위타입의 프로토타입을 할당하기 위해 상위 타입의 생성자를 호출할 필요는 없다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function inheritPrototype(subType, superType) {
var prototype = object(superType.prototype);
prototype.constructor = subType;
subType.prototype = protoType;
}

function SuperType(name) {
this.name = name;
this.colors = ["red", "blue", "green"];
}

SuperType.protoype.sayName = function() {
alert(this.name);
}

function SubType(name, age) {
SuperType.call(this, name);
this.age = age;
}

inheritPrototype(SubType, SuperType);

SubType.prototype.sayAge = function() {
alert(this.age);
};
  1. 상위 타입의 프로토타입을 복제
  2. consturctor 프로퍼티를 prototype에 할당하여, 기본 constructor 사라지는 현상 대비
  3. 하위타입의 프로토타입에 새로 생성한 객체를 할당

SuperType 생성자를 단 한 번만 호출하므로 SubType.prototype에 불필요하고 사용하지 않는 프로퍼티를 만들지 않았다는 점에서 효과적. 포로토타입 체인이 온전히 유지되므로 instanceof와 isPrototypeOf() 메서드로 정상 작동.

가장 효율적인 상속 패러다임으로 평가.

Reference

프론트엔드 개발자를 위한 Javascript - 니콜라스 자카스