목차

  1. 프로토타입
    1.1. 프로토타입이란?
    1.2. 프로토타입 체인
  2. 생성자 함수 vs 객체 리터럴 반환 함수
    2.1. 반환 객체의 프로토타입
  3. 프로토타입을 가지지 않는 함수
    3.1. 함수 목적성 확실하게 구분
  4. 참고 자료

프로토타입

이전 함수 정리 게시글에서 생성자 함수와 객체 반환 함수의 차이를 알아보았습니다. 이 두 함수의 차이점은 바로 프로토타입입니다. 해당 게시글에선 단순하게 '생성자 함수와 객체 리터럴 반환 함수가 생성한 객체는 서로 다른 프로토타입을 가진다. 생성자 함수가 생성한 인스턴스는 생성자 함수의 프로토타입을 가지며 객체 리터럴 반환 함수의 경우 모든 객체가 가지는 기본 프로토타입 Object.prototype을 가진다.' 정도로만 정리하였습니다. 이때 프로토타입이 정확히 무엇인지 왜 사용되는지는 알아보지 않고 넘어갔었는데 이번글에서 프로토타입에 대해 자세히 알아보겠습니다.

생성자 함수가 생성한 객체 (인스턴스)객체 리터럴 반환 함수가 생성한 함수
프로토타입생성자 함수의 프로토타입모든 객체가 가지는 프로토타입 Object.prototype

그래서 객체 리터럴이 뭐야?

우선 객체 리터럴이 무엇인지 명확하게 짚고 넘어가겠습니다. 리터럴이란 자바스크립트에서 사용되는 값들을 나타냅니다. 배열 리터럴, 불리언 리터럴, 숫자 리터럴, 객체 리터럴 등 다양한 리터럴이 존재합니다. 이것들 중 객체 리터럴은 중괄호({})로 묶인 0개 이상의 객체의 속성 이름과 그와 연관된 값의 쌍의 목록입니다.

정리가 필요한 의문 사항들
프로토타입에 대해 공부하며 아래 의문 사항들을 해결할 것 입니다.

  • 생성자 함수도 객체를 리턴하는 함수인데 그냥 객체 리턴 함수랑 뭐가 다르지?
  • 프로토타입 덕분에 메모리 효율이 좋아진다던데 이유는?
  • 그냥 객체 리턴 함수만 사용하면 안되나? 이 방법이 단순해서 코드 작성이 편한데..

프로토타입?

프로토타입 기반 언어

자바스크립트의 모든 객체들은 프로토타입 객체를 가지고 있습니다. 이 프로토타입 객체는 속성과 메소드를 가진 일종의 템플릿 역할을 하며 상속을 가능하게 합니다. 이러한 특성으로 자바스크립트를 프로토타입 기반 언어라 합니다.

프로토타입 상속

자바스크립트의 상속은 크게 두가지 유형으로 나눌 수 있습니다. 하나는 생성자와 인스턴스 사이의 상속이며 다른 하나는 생성자 함수 간의 상속입니다.

생성자와 인스턴스 사이의 상속

위에서 자바스크립트의 모든 객체는 프로토타입 객체를 가진다고 언급하였습니다. 생성자 함수 또한 프로토타입 객체를 가지고 있습니다. 그렇기 때문에 생성자 함수와 인스턴스 사이에서 상속이 가능한 것입니다. 하나의 생성자 함수로 여러개의 인스턴스가 만들어지면 이 인스턴스들은 모두 생성자 함수의 프로토타입을 물려받습니다. 이러한 상속이 생성자와 인스턴스 사이의 상속입니다.

// 생성자 함수
function Animal(sound) {
  this.sound = sound;
  this.someMethod = function() {
	  console.log("나는 공유되지 않아요!");
  }
}

// 생성자 함수 프로토타입에 메소드 추가
Animal.prototype.makeSound = function() {
  return this.sound + '!!!';
};

// 인스턴스 생성
var dog = new Animal('멍멍');

// 상속받은 메소드 사용
console.log(child.makeSound()); // '멍멍!!!'

Animal 생성자 함수가 선언된 후 생성자 함수의 prototype 속성에 접근하여 makeSound 메소드가 추가되었습니다. 이후 생성된 dog 인스턴스는 Animal 생성자 함수의 프로토타입을 물려받습니다. 그렇기 때문에 makeSound 메소드 사용이 가능합니다.

🚨🚨🚨 주의사항

생성자 함수 내부에서 정의한 속성들도 프로토타입 객체에 포함되는 것으로 착각할 수 있습니다. 하지만 위 코드에서 속성 값 sound는 Animal 생성자 함수의 프로토타입 객체에 포함되지 않습니다. 그저 Animal 생성자 함수로 생성된 인스턴스들이 가지는 고유한 값으로 독립되어 있습니다. Animal 생성자 함수로 생성된 인스턴스들이 공유할 수 있는 메소드는 프로토타입 속성에 추가한 makeSound 메소드뿐입니다.

위 코드의 someMethod 메소드 처럼 생성자 함수 내부에서 정의된 경우 생성된 인스턴스들이 모두 복제된 값을 가지게 됩니다. 결과적으로는 모든 인스턴스가 복제된 메소드를 가지고 있어 메소드 사용이 가능합니다. 하지만 모든 인스턴스가 복제된 메소드를 가지면 이것이 모두 메모리에 저장되기 때문에 하나의 메소드를 메모리에 가지고 참조하여 공유하는 것보다 효율적이지 못합니다. 따라서 프로토타입 객체에 메소드를 추가하는 방식으로 메소드를 추가해야합니다.

생성자 함수 간의 상속

생성자 함수 간의 상속은 한 생성자 함수가 다른 생성자 함수의 프로토타입을 상속받는것을 뜻합니다. 이 경우 하위 생성자 함수가 만든 인스턴스가 상위 생성자 함수의 프로토타입에 접근이 가능합니다. 주의해야할 사항은 상위 생성자 프로토타입에 접근이 가능하다는것입니다. 즉 상위 생성자 프로토타입을 하위 생성자 인스턴스가 복사하여 가지는 것이 아닙니다.

// 상위 생성자 함수
function Animal(legCount) {
  this.legCount = legCount;
}

// 상위 생성자 프로토타입 메소드
Animal.prototype.walk = function() {
  return 'This animal walks on ' + this.legCount + ' legs.';
};

// 하위 생성자 함수
function Bird(legCount, wingCount) {
  Animal.call(this, legCount); // 상위 생성자 함수에게 `this`와 매개변수를 전달
  this.wingCount = wingCount;
}

// 하위 생성자의 프로토타입을 상위 생성자의 프로토타입으로 설정
Bird.prototype = Object.create(Animal.prototype);

// 하위 생성자의 constructor 속성을 복원
Bird.prototype.constructor = Bird;

// 하위 생성자 프로토타입 메소드 추가
Bird.prototype.fly = function() {
  return 'This bird flies with ' + this.wingCount + ' wings.';
};

// 하위 생성자 인스턴스 생성
var sparrow = new Bird(2, 2);

// 상속받은 메소드 사용
console.log(sparrow.walk()); // 'This animal walks on 2 legs.'
console.log(sparrow.fly()); // 'This bird flies with 2 wings.'

위 코드에서 Animal 생성자 함수와 Bird 생성자 함수가 존재합니다. Bird 생성자 함수는 Animal 생성자 함수를 상속 받습니다. 그렇기 때문에 Bird 생성자 함수가 생성한 인스턴스는 Animal 생성자 함수의 프로토타입에 접근이 가능합니다.

객체 리터럴의 프로토타입

생성자 함수가 만든 인스턴스가 아닌 객체 리터럴은 Object.prototype 프로토타입을 가집니다. 이는 자바스크립트의 모든 객체가 가지는 가장 기본이되는 프로토타입입니다.

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

const dog = new Animal('댕댕이');
const cat = {
	name : '냐옹이'
}

console.log(dog);
console.log(cat);

위 코드에서 출력된 결과를 보면 dog 인스턴스의 프로토타입 속성 값은 Animal.prototype입니다. 반면에 cat 객체의 프로토타입 속성 값은 Object.prototype입니다.

자바스크립트의 모든 객체는 기본적으로 Object.prototype 프로토타입 속성을 가지고 있습니다. 그렇기 때문에 dog 인스턴스도 Object.prototype 프로토타입을 가집니다. 이는 프로토타입 체인을 통해 접근 가능합니다. 변수 cat의 값은 단순 객체 리터럴이기 때문에 Object.prototype 프로토타입을 가집니다. 이는 가장 기본이 되는 프로토타입이기 때문에 더 이상 상위로 올라가 접근이 불가능합니다.

프로토타입 체인

위에서 생성자 함수 간의 상속을 보았습니다. 이 상속 관계에서 상위 생성자 함수와 하위 생성자 함수가 결정됩니다. 이때 하위 생성자 함수의 인스턴스가 상위 생성자 함수의 프로토타입에 접근 가능합니다. 이 덕분에 해당 객체에 원하는 속성이나 메소드가 없을 때 상위 생성자 함수의 프로토타입에 접근해 원하는 속성이나 메소드를 탐색가능합니다. 이 과정에서 원하는 속성이나 메소드를 찾지 못한 경우 최상위 프로토타입인 Object.prototype에 도달할 때까지 계속 탐색합니다. 이러한 연쇄적인 탐색 과정을 프로토타입 체인이라고 합니다.

function A(){
	...
}
A.prototype.aMethod = function() {
	console.log('a method');
}


function B(){
	...	
}
B.prototype = Object.create(A.prototype);
B.prototype.constructor = B;
B.prototype.bMethod = function() {
	console.log('b method');
}


function C(){
	...
}	
C.prototype = Object.create(B.prototype);
C.prototype.constructor = C;
C.prototype.cMethod = function() {
	console.log('c method');
}

const c = new C();
c.aMethod(); //'a method'

위 코드에서 생성자 함수 B는 생성자 함수 A의 프로토타입을 물려받고 있습니다. 생성자 함수 C는 생성자 함수 B의 프로토타입을 물려받고 있습니다. 인스턴스 c는 프로토타입 객체에 cMethod 메소드만 가지고 있습니다. 그런데 aMethod 메소드를 호출하면 a method가 출력됩니다. 이것이 가능한 이유가 바로 프로토타입 체인입니다.

c 인스턴스가 aMethod를 호출하기 위해 가장 먼저 자신의 프로토타입 객체를 확인합니다. 하지만 여기엔 aMethod가 존재하지 않습니다. 여기서 호출 불가로 코드를 끝내지 않고 메소드를 찾기위해 상위 B 프로토타입에 접근합니다. 여기에도 aMethod가 없어 더 상위 프로토타입인 A 프로토타입에 접근하고 aMethod를 찾아 호출합니다.

생성자 함수 vs 객체 리터럴 반환 함수

반환 객체의 프로토타입

생성자 함수는 인스턴스를 반환합니다. 생성된 인스턴스는 생성자 함수의 프로토타입을 물려 받습니다. 반면에 객체 리터럴 반환 함수는 객체 리터럴을 반환합니다. 이 객체 리터럴은 가장 기본이 되는 프로토타입인 Object.prototype을 가집니다. 이것이 생성자 함수와 객체 리터럴 함수를 구분 짓는 가장 큰 차이점 입니다.

생성자 함수와 객체 리터럴 반환 함수가 여러개의 객체(인스턴스)를 생성한 경우를 상상해보겠습니다. 이 경우 생성자 함수가 생성한 인스턴스는 모두 생성자 함수의 프로토타입을 물려받습니다. 이는 모든 인스턴스가 프로토타입을 복제하여 가지고 있다는 뜻이 아니라 프로토타입을 참조로서 물려 받고 있는것 입니다. 인스턴스가 n개 생성된어도 생성자 함수 프로토타입 1개를 참조하는 형태로 상속이 이루어집니다. 즉 생성자 함수의 프로토타입이 메소드를 지니고 있다면 메모리는 한 공간에 저장되고 n개의 인스턴스들이 메소드를 참조하여 호출가능합니다. 메소드가 n개 복제되는 것이 아니기 때문에 인스턴스를 많이 생성하는 경우 매우 효율적입니다. 따라서 생성자 함수는 여러 객체를 찍어내는 틀 역할로 적합합니다.

하지만 객체 리터럴 반환 함수가 n개의 객체를 생성하는 경우 n개의 객체가 Object.prototype을 프로토타입으로 가집니다. 생성된 객체가 서로 공유하는 프로토타입이라고는 최상단 프로토타입인 Object.prototype이 되는것입니다. 그렇기 때문에 프로토타입을 통해 얻는 이점이 없습니다. 만약 반환된 객체 리터럴의 속성 값으로 함수가 존재한다면 이 함수는 모든 객체에 복제됩니다. 공유하는 것이 아니라 모두 복제되는 것이므로 메모리에 모두 공간을 차지합니다. 따라서 n개의 객체가 생성되면 n개에 메소드가 메모리에 점유됩니다. 이는 객체가 많아지는 경우 매우 비효율적입니다. 따라서 객체 리터럴 반환 함수는 여러개의 객체를 찍어내는 틀 역할로 부적합합니다.

function Animal(sound){
	this.sound = sound;
}
Animal.prototype.makeSound = function() {
	console.log(sound+'!!!');
}

function Animal2(sound){
	return {
		sound,
		makeSound : function() {
			console.log(sound+'!!!');
		}
	}
}

const 생성자1 = new Animal('멍');
const 생성자2 = new Animal('왈');
const 생성자3 = new Animal('냐옹');

const 리터럴1 = Animal2('멍');
const 리터럴2 = Animal2('왈');
const 리터럴3 = Animal2('냐옹');

생성자 함수 Animal의 인스턴스 생성자1, 생성자2, 생성자3는 Animal.prototype을 공유합니다. 이는 참조로서 공유하는 것이기 때문에 메모리 한 공간에 makeSound 메소드가 저장되고 인스턴스들이 이를 참조하여 호출가능합니다. 메소드는 인스턴스의 수가 증가해도 추가 공간을 요구하지 않습니다.

반면에 객체 리터럴 반환 함수 Animal2가 생성한 객체인 리터럴1, 리터럴2, 리터럴3은 모두 makeSound 메소드를 복제하여 가지고 있습니다. 생성된 객체마다 모두 메모리에 메소드를 저장하기 때문에 객체의 수가 증가할 수록 메모리에 차지하는 공간이 늘어납니다.

정리하자면 생성자 함수로 생성된 인스턴스들은 공유된 생성자 함수의 프로토타입을 참조해서 메소드를 호출합니다. 객체 리터럴 반환 함수가 생성한 객체들은 복제된 메소드를 본인 스스로가 가지고 있고 그것을 호출합니다. 이 차이로 객체를 생성하는 두 방식이 다른 메모리 효율을 가지게 됩니다.

프로토타입을 가지지 않는 함수

생성자 함수는 프로토타입을 가지고 생성되는 인스턴스들에게 이 프로토타입을 물려줍니다. 그렇다면 생성자 함수만 프로토타입을 가지는 걸까요? 아닙니다. 생성자 함수가 아니더라도 프로토타입을 가질 수 있습니다. 정확히는 function 키워드로 선언된 함수들은 모두 프로토타입을 가집니다. 반면에 화살표 함수는 프로토타입을 가지지 않습니다.

//생성자 함수
function A(name){
  this.name = name;
}
//익명 함수를 사용한 함수 표현식
const B = function(name) {
  ...
}
//화살표 함수를 사용한 함수 표현식
const C = (name) =>{
  ...
}

함수 A는 생성자 함수이고 함수 B는 익명 함수를 이용한 함수 표현식입니다. 둘다 function 키워드로 함수가 선언되었기 때문에 프로토타입을 가지고 있습니다. 함수 C는 화살표 함수를 이용한 함수 표현식입니다. 이 경우 프로토타입을 가지지 않습니다.

프로토타입은 생성자로 활용될 때 사용됩니다. 따라서 함수 A, B 모두 new 키워드를 이용해 객체를 생성할 수 있습니다. 하지만 함수 C의 경우 프로토타입이 없어 new 키워드를 이용해 객체를 생성할 수 없습니다.

함수 목적성 확실하게 구분

함수가 프로토타입을 가지면 결국 메모리를 사용합니다. 따라서 생성자와 관련이 없는 함수가 프로토타입을 가지는 것은 메모리가 낭비되는 것입니다. 따라서 생성자 함수와 일반적인 함수를 구분짓는것이 유용합니다. 프로토타입이 필요한 생성자 함수는 function 키워드를 사용한 함수 선언문, 프로토타입이 필요 없는 일반 함수는 화살표 함수를 이용한 함수 표현식으로 구분지어 사용하면 프로토타입이 차지하는 메모리 낭비를 막을 수 있습니다.

사실 생성자 함수가 필요할때 es6에서 추가된 클래스 문법을 활용하는 것이 더 유용합니다. 관련 글은 다음에 정리해보도록 하겠습니다.

생성자 함수 - 클래스 문법
일반 함수 - 화살표 함수
메소드 함수 - 메소드 축약형

참고자료

profile
프론트엔드 개발자

0개의 댓글