11. 프로토타입

11-1. 프로토타입 사용 사례

자바스크립트에서 객체 지향 프로그래밍을 구현하는 방식으로는 프로토타입 상속, 객체 리터럴 등이 있다. 모든 자바스크립트의 함수는 프로토타입을 가지며, 빌트인 오브젝트 또한 프로토타입이 있다. 만약 프로토타입이 없으면 함수는 수명이 끝나며 해당 함수의 인스턴스가 메모리에서 해제된다.
 
프로토타입을 가지는 빌트인 오브젝트들
notion imagenotion image
 

11-2. 프로토타입의 정의

모든 객체는 프로토타입 객체를 가지며, 이 프로토타입 객체는 해당 객체의 기능과 속성을 정의한다. 객체의 속성과 메서드는 프로토타입 객체에서 상속된다. 이렇게 상속 관계를 통해 ‘객체’ 간에 공통된 기능을 재사용할 수 있다.

11-2-1. 생성자 함수와 프로토타입

생성자 함수를 만들면 자동으로 프로토타입이 포함된다.
// 생성자 함수 function Person(name) { this.name = name; } // 프로토타입에 메서드 추가 Person.prototype.greet = function() { console.log(`Hello, my name is ${this.name}`); }; // 객체 생성 const person1 = new Person('Alice'); const person2 = new Person('Bob'); // 메서드 호출 person1.greet(); // 출력: Hello, my name is Alice person2.greet(); // 출력: Hello, my name is Bob
위 방법처럼 Person을 정의하여 person1와 person2의 greet() 메서드를 호출하여 사용할 수 있다
 
 

11-2-2. 생성자와 프로토

비슷한 객체들은 비슷한 작업을 수행할 수 있어야 하므로, 공통된 기능을 공유하는 것이 효율적이다. 하지만 때로는 객체들이 각자 독립적인 작업을 수행해야 하는 상황도 있다.
function Taiyaki(){ this.filling = "팥"; } const taiyaki1 = new Taiyaki(); const taiyaki2 = new Taiyaki(); //taiyaki1의 프로토에 산입 taiyaki1.__proto__.setFilling = function(ReFilling){ this.filling = ReFilling return this.filling+"으로 채운다" } //taiyaki1의 포로토타입에 산입 taiyaki1.eat = function(newFilling){ return this.filling + "을 먹다" } console.log(taiyaki1.setFilling("크림")) // 크림으로 채운다 console.log(taiyaki1.eat()) // 크림 먹다 console.log(taiyaki2.setFilling("치즈")) // 치즈으로 채운다 console.log(taiyaki2.eat()) // 에러
위의 로직을 보면 console.log(taiyaki2.eat()) 에서 에러가 떠야 한다. 아직 팔리지 않은 taiyaki2 는 자신이 어떠한 목적으로 구매하였는지 모르기 때문에 메소드를 추가 하지 않는 것이 올바르다. 그래서 eat()이라는 메서드를 사용할 수 없게 해야 한다.
taiyaki1.__proto__.setFilling = function(ReFilling){ this.filling = ReFilling return this.filling+"으로 채운다" }
반면 setFilling 이라는 내용을 바꾸는 상태는 모든 객체가 알 수 있어야 한다.
그러하기 위해 proto 에 함수를 넣었다 proto 에 함수를 넣은 자원은 같은 생성자를 사용한 객체에 공유한다.
taiyaki1.eat = function(newFilling){ return this.filling + "을 먹다" }
먹는다는 판단은 팔린 객체 taiyaki1만 알아야 한다. 만일 먹지 않고 다른 사람한테 양도하거나 보관한다는 경우가 발생할 수도 있고, 결국 마지막엔 먹지도 못하고 버릴 수도 있다. 그래서 taiyaki1 프로토타입을 넣어서 해당 객체만 실행할 수 있는 함수를 넣어, 다른 객체에서 오류를 일으킬 만한 메서드를 주지 않는 것이 바람직하다.
 

11-2-3. 같은 값을 가진 프로토

객체에서 프로토타입 객체를 가리키는 링크를 가지며, 이 링크를 따라 상위 프로토타입 객체를 찾는 과정을 프로토타입 체인이라고 한다. 만약 객체에서 어떤 속성이나 메서드를 찾을 수 없을 때는 이 체인을 따라 상위 프로토타입 객체에서 찾는다.
생성자 함수를 사용하면 기본적인 프로토타입은 공유된다.
function Person(name) { this.name = name; } let person = new Person('John'); console.log(person.__proto__=== Person.prototype); // true
위처럼 프로토타입을 그대로 들고 와서 부모에 있는 prototype과 __proto__는 완전히 같다.
이렇게 완전히 같으면 두 개의 클래스를 합치는 것 또한 가능하다.
// 부모 생성자 함수 function Animal(name) { this.name = name; } // 부모 생성자 함수의 프로토타입에 메서드 추가 Animal.prototype.sayName = function() { console.log(`I am ${this.name}`); }; // 자식 생성자 함수 function Dog(name, breed) { Animal.call(this, name); // 부모 생성자 호출 this.breed = breed; } // 자식 생성자 함수가 부모 생성자 함수의 프로토타입체인을 상속 Dog.prototype = Object.create(Animal.prototype); // 자식 생성자 함수에 추가된 메서드 Dog.prototype.bark = function() { console.log('Woof!'); }; // 객체 생성 const dog = new Dog('Buddy', 'Golden Retriever'); // 메서드 호출 dog.sayName(); // 출력: I am Buddy dog.bark(); // 출력: Woof!
위처럼 두 가지 프로토타입을 합쳐서 사용하는 것 또한 가능하다.

11-2-4. 프로토타입 간의 소통 공유 범위

function Animal(name) { console.log(this.name + '함수 안'); } // 부모 생성자 함수의 프로토타입에 메서드 추가 Animal.prototype.getName = function () { this.setName(); console.log(this.name + '프로토타입 안'); }; Animal.prototype.setName = function () { this.name = 100; }; const myAnimal = new Animal('Fido'); myAnimal.getName(); // undefined 함수 안, Fido는 function Animal 안으로 들어가지 않는다. // 100 프로토타입 안, getName을 호출 시 Fido가 무시되고 100이 들어간다.
Animal은 ‘생성자 함수’이고 getName은 ‘프로토타입’에 있다. 따라서, this는 각각 다른 객체를 가리킨다. Animal 함수 내에서의 this는 새로 생성된 Animal 객체를 가리키고, setName 함수 내에서의 this는 동일한 Animal 객체를 가리킨다. getName 함수를 호출할 때, setName 함수에서 설정된 this.name 값인 100이 출력된다.
 

11-3. 현업에서 프로토타입과 생성자를 사용 방법

다음은 메서드 체이닝과 프로토타입, 아규먼트의 값을 활용한 예제코드이다.
function Vector(x, y) { this.x = x || 0; this.y = y || 0; } Vector.prototype = { // 입력값이 하나이면 {x.x x.y} 두 개이면 {x, y} add: function (x, y) { if (arguments.length === 1) { // 값이 하나이면 아래 로직 실행 this.x += x.x; this.y += x.y; } else if (arguments.length === 2) { // 값이 두 개이면 아래 로직 실행 this.x += x; this.y += y; } // 메서드 체이닝을 위한 반환값 this return this; }, sub: function (x, y) { if (arguments.length === 1) { this.x -= x.x; this.y -= x.y; } else if (arguments.length === 2) { this.x -= x; this.y -= y; } return this; }} Vector.sub = function (v1, v2) { // 아규먼트를 받고 v1에서 v2를 빼 다시 벡터를 만든다. // 하지만 위에 프로토타입에 sub를 정의해서 아규먼트 개수가 한 개인지 두 개인지 확인하고 적용한다. return new Vector(v1.x - v2.x, v1.y - v2.y); }; Vector.add = function (v1, v2) { // 위에 프로토타입에 add를 정의해서 아규먼트 개수가 한 개인지 두 개인지 확인하고 적용한다. return new Vector(v1.x + v2.x, v1.y + v2.y); }; const v1 = new Vector(3, 4); const v2 = new Vector(2, 3); const resultAdd = Vector.add(v1, v2); const resultSub = Vector.sub(v1, v2); console.log('덧셈 결과: (' + resultAdd.x + ', ' + resultAdd.y + ')');// 덧셈 결과: (5, 7) console.log('뺄셈 결과: (' + resultSub.x + ', ' + resultSub.y + ')');// 뺄셈 결과: (1, 1) const v3 = {x:2,y:2}; const resultOneArgAdd =v1.add(v3); console.log('스칼라 덧셈 결과: (' + resultOneArgAdd.x + ', ' + resultOneArgAdd.y + ')');// 스칼라 덧셈 결과: (5, 6) const resultOneArgSub = v1.sub(v3); console.log('스칼라 뺄셈 결과: (' + resultOneArgSub.x + ', ' + resultOneArgSub.y + ')');// 스칼라 뺄셈 결과: (3, 4) const v4 = new Vector(2, 3); // 메서드 체이닝 v4.add(1, 2).sub(2, 1); console.log(v4)// { x: 1, y: 4 }
백엔드 기준에서는 다양한 디자인 패턴과 함께 사용하지만 위 프로그램은 프론트엔드에서도 캔버스 등에 사용하는 로직이다.
this.x += x; this.y += y;
위 로직으로 각각의 this에 있는 x와 y를 증가시킨다.
Vector.prototype = { add: function (x, y) { if (arguments.length === 1) { this.x += x.x; this.y += x.y; } else if (arguments.length === 2) { this.x += x; this.y += y; } return this; },
마지막으로 메서드 체이닝은 메서드를 호출한 후에도 메서드를 반환하여 연속적으로 호출을 가능하게 하는 패턴이다. 이 패턴은 보통 메서드가 자기 자신을 반환함으로써 체이닝을 가능하게 하다.
v4.add(1, 2).sub(2, 1);