[js] 클래스 기반 언어 vs 자바스크립트 2/3

자바스크립트에 클래스가 없다는 점을 앞 서 설명했습니다. 하지만 클래스는 있을 만 하니까 만든 것이 아니겠사옵니까.
이번에는 클래스 기반 언어가 클래스를 통해 해결하던 문제를 자바스크립트에서는 어떻게 해결하는지 알아보는 첫 번째 순서로 프로토타입을 집중 탐구해보겠습니다.

  • 물론 이 시리즈는 자바스크립트 언어에 대해 다루고는 있습니다만, 전반적인 프로그래밍 언어의 다양한 개념을 마구 사용하고 있어 언어와 언어관련 용어에 대한 이해가 없으신 분에겐 다양한 압박을 가할 수 있습니다만…..어쩔 수 없습니다…
    ..더 쉽게 풀어쓸 능력이 없기 때문에..=.=;

 
 
 

정의된 클래스의 인스턴스

클래스의 주 기능은 클래스를 통해 생성된 인스턴스가 어떤 모양인지 정의하는 것입니다.

여기서 말하는 모양은 인스턴스의 필드와 메서드로 나눌 수 있습니다.

클래스가 없는 자바스크립트 입장에서 일단 오브젝트를 하나 만든 뒤 필드와 메서드를 셋팅하는 과정은 일종의 매크로로 볼 수 있습니다.

먼저 이 전 포스팅에 예제로 등장했던 alert 클래스를 살펴보겠습니다.

class Alert{

	private String _msg;

	public alert( String $msg ){
		_msg = $msg;
	}

	public void action(){
		System.out.print( _msg );
	}
}

Alert클래스에는

  1. _msg 필드 한 개와
  2. _msg 를 초기화 해주는 생성자,
  3. 마지막으로 콘솔에 출력하는 action 메서드가 있습니다.

이 세 가지 부속을 자바스크립트로 제작합니다.

먼저 생성자 대신 그 역할을 수행할 함수를 하나 정의할 텐데, 내부에서 할 일을 생각해보면 오브젝트를 하나 만든 뒤 _msg action 함수를 키로 잡아서 반환하는 것입니다.

function Alert( $msg ){
 	var result;
 	result = {_msg:$msg, action:action};
 	return result;
 }

근데 action 함수는 어디에 정의하는 게 합당할까요? 여기에는 두 가지 방법이 상황에 맞게 사용됩니다.

  1. 프로토타입에 정의하기
  2. 스코프에 정의하기

위의 방법은 모두 중요한 자바스크립트의 설계방법으로 이 포스팅에서는 프로토타입에 대해 깊이 살펴보겠습니다.

 
 

new 보강

복잡한 프로토타입 시스템에 들어가기 전에 new 연산자에 대해서 한 가지 보강을 하고 가겠습니다.

초간단 복습하면 어떤 함수가 new 연산자 뒤에 오는 경우

  1. 오브젝트를 생성하고,
  2. __proto__ 에 프로토타입 넣고,
  3. 함수에 컨텍스트를 방금 만든 객체로 해서 실행한다

고 했습니다. 이 중 3번 항목의 생성자 실행 부분만 더 파고 들어 봅니다.

우선 위의 함수 그대로를 생각해보면 Alert 는 아무것도 반환하지 않고 있습니다. 이는 내부적으로 undefined 를 반환한 것과 같습니다.

function Alert( $msg ){
	this._msg = $msg;
	//또는 return 또는 return undefined;
}

이 경우는 그냥 새로 생성된 오브젝트가 그대로 변수에 할당됩니다. 즉 아래와 같습니다.

var a = new Alert( "hello" );
a._msg == "hello" //true

하지만 함수가 무언가 반환하면 재밌어집니다.

기본형을 반환하면 위와 같이 새로 생성된 객체가 들어옵니다.

function Alert( $msg ){
	this._msg = $msg;
	return 3; //기본형 반환
}

var a = new Alert( "hello" );
a._msg == "hello" //true

하지만 만약 객체형을 반환하면 그 반환된 객체로 대체되어버립니다.

function Alert( $msg ){
	this._msg = $msg;
	return {}; //객체형 반환
}

var a = new Alert( "hello" );
a._msg == "hello" //false = undefined

이 때 들어온 a 는 마지막에 반환받은 {} 가 되어버려서 전혀 다른 객체가 되어버린거죠. 그럼 이 때 new 로 생성된 객체는 어디로 갔을까요?

어디로 갔는지는 모르겠지만 생성 자체는 확실하게 되었다는 증거를 보여드릴 수 있습니다.

var ghost;
function Alert( $msg ){
	this._msg = $msg;
	ghost = this;
	return {}; //객체형 반환
}

var a = new Alert( "hello" );
a._msg == "hello" //false = undefined

ghost._msg == "hello" //true

함수 내부에서 this 참조를 외부 변수에 잡아서 확인해보면 생성이 안된 건 아닙니다. 하지만 반환이 다른 객체로 되는 것 뿐입니다. 이러한 구조를 이용하면 외부에는 가짜 객체만 넘겨주고 생성된 인스턴스는 죄다 내부에서 보관하는 전략을 쓰는 것처럼 다양한 응용이 가능합니다.

진정한 의미에서 new 의 매크로는 다음과 같을 겁니다.

var a = new Alert( "hello" );

//1
a = {};

//2
a.__proto__ = Alert.prototype;

//3
temp = Alert.apply( a, arguments );
if( typeof temp == 'object' || typeof temp == 'function' ) a = temp;

 
 
 

함수의 프로토타입 시스템

일단 소스를 먼저 보고 생각해보죠.

function Alert( $msg ){
	this._msg = $msg;
}

Alert.prototype.action = function(){
	alert( this._msg );
};

var a = new Alert( 'test' );
a.action();//test출력

일단 이 구문의 장점은 자바 개발자들에게 마음의 평온을 준다는 것입니다. 일견 클래스와 new 를 사용하는 듯한 착시를 가져다 주기 때문에 험난한 js 사파리에서 인지부조화를 벗어날 수 있게 해주는 효과가 있죠.

(하지만 현실은 전혀 다르고 오히려 좀 가혹합니다 ㅎㅎ)
 
 

함수가 생성될 때 일어나는 일

위의 코드를 제대로 이해하려면 복잡한 자바스크립트의 함수가 내부에서 어떻게 작동하는지 이해해야 합니다.

하지만 정확하게 말해서 이해가 아닙니다. 이는 전부 브랜든 할베가 개인적으로 정한 것입니다. 따라서 이해라기보다는 그 정해둔 사실을 외우는 것입니다.

자바스크립트를 공부할 때 짜증나는 부분은 많은 내부 시스템이 어떤 규칙이나 원리가 있는 게 아니라 그저 브랜든 할베가 그렇게 정한게 대 부분이란 겁니다.

원리 같은 게 없으니 그냥 생각하지 말고 외워야 합니다.

 

또한 내부 시스템 대 부분이 무언가를 특별한 걸 한다기보다는, new 처럼 그저 매크로나 배치작업에 가까운 것이 많습니다. 귀찮은 과정을 자동으로 해주는 거죠.

이러한 기조는 개발자의 귀찮음을 줄인다기보다, 호스트코드를 작성할 때 브렌든 할베가 생각한 방향으로 개발하길 원해서 정해둔 게 다분히 많습니다(개인적인 생각입니다 ^^)

쨌든 요점은 자바스크립트에서는 함수를 생성하면

  1. 즉시 함수에 prototype 키를 만들고 오브젝트를 할당합니다.
  2. 또한 prototype 오브젝트 안에는 constructor라는 키가 잡히고 값으로 함수 자신의 참조가 들어갑니다.

코드로 표현 해보죠.

// 함수가 생성되면
function Alert(){}

// 1. 즉시 prototype이라는 이름의 키에 오브젝트가 생성되고
Alert.prototype = {};

// 2. constructor가 셋팅된다.
Alert.prototype.constructor = Alert;

이 과정은 절대로 일어나고 c 수준의 파서에서 관리하는 영역입니다.
따라서 prototype은 함수 객체를 만들면

  1. 함수 객체에 정의되는 특별한 키고
  2. 그 키에 할당된 객체는 특별히 constructor 키가 언제나 정의 되어있다

라는 사실을 알 수 있습니다. 하지만 인터프리터가 해주는 것은 그저 매크로와 같이 대신해 줄 뿐, 무슨 독점적인 권한을 갖고 있는게 아닙니다. 예를 들어 constructor 를 바꿔봅시다.

function Alert(){}
Alert.prototype.constructor = Array;

var temp = new Alert;
temp.constructor === Array //true

그러니 이런 장난이 가능해집니다. constructor 에 일단 Alert 를 써주긴 하지만 짜피 해시맵인데 바꾸지 못할 것도 없습니다. 따라서 constructor 로 형을 판단하는 건 매우 위험한 짓 입니다.
컴파일러를 속일 수 없는 형검사는 instanceof 연산자입니다.

function Alert(){}
Alert.prototype.constructor = Array;

var temp = new Alert;
temp instanceof Alert //true

//심지어 prototype을 제거해도 된다.
Alert.prototype = null;
temp instanceof Alert //true

prototype 을 제거하는 얘기가 나왔으니 하는 말인데, prototypenull 을 할당하면 확실히 모든 체인은 끊어지긴 합니다.

그런데도 신기하게 Object.prototype 에 있는 toString 등은 살아있습니다. 결론적으로 다음과 같은 생각에 도달합니다.

function setPrototype( $prototype ){
	this.prototype = $prototype || {};
}

이게 function prototype 을 위한 내부 setter 에 내장 되어 있다고 봐야겠죠. 암튼 자바스크립트 코드 상으로는 prototype 을 제거하든 constructor 에 다른 걸 할당하거나 제거하는 것도 가능하고 실제로 작동도 합니다.

이제 곧 복잡해지니 확실하게 기억하면서 전진해보죠.
 
 

prototype을 new와 함께 사용하기

지겹지만 다시 new 가 어떻게 작동되는지 살펴봅시다.

var a = new Alert( 'test' );

//1
a = {};

//2
a.__proto__ = Alert.prototype;

//3
temp = Alert.apply( a, arguments );
if( typeof temp == 'object' || typeof temp == 'function' ) a = temp;

위의 2번 단계를 살펴보면 new 구문을 통해 Alert.prototype 에 대한 참조가 a 에게 __proto__ 로 잡혀있음을 알 수 있습니다. 이것만 집중적으로 보려합니다. 함수의 정의부터 다시 복습해보면

function Alert( $msg ){
	this._msg = $msg;
}
Alert.prototype.action = function(){
	alert( this._msg );
};

var a = new Alert( 'test' );
a.__proto__ = Alert.prototype;//2

이 코드를 새삼 다시 보면 아래와 같은 결론에 도달합니다.

a.__proto__ == Alert.prototype;
a.__proto__.action == Alert.prototype.action;

참조가 같으니 Alert 의 프로토타입에 정의된 모든 속성은 전부 a.proto 도 참조할 수 있게 됩니다. 특히 constructor 도 참조됩니다.

//new 하면 constructor가 참조되는 비밀이 풀렸다!
a.__proto__.constructor == Alert.prototype.constructor == Alert;

뭐 여기까지 해서 a.proto.action 이 존재한다는 건 알게 되었습니다. 근데 아래 코드가 가능한 이유는 무엇인가요?

a.action();

 
 

__proto__가 작동하는 방식

a.action()…이건 자바스크립트 파서가 어떻게 작동하는지의 문제입니다. 일단 자바스크립트 파서는 a.action 에서 a 라는 객체에 action 이라는 키가 있는지 찾아봅니다.

근데 a에는 _msg 라는 키만 있을 뿐 action 이라는 키가 없습니다. 그럼 그 다음에 a.proto 를 찾게 되는거죠. 코드로 표현하면 아래와 같을 것입니다.

function getValue( $key ){

	var result;
	result = this;

	do{
		if( result[$key] ) return result[$key];

	}while( result = result.__proto__ )

	return undefined;
}

즉 일단 자신의 키를 뒤져보고 있으면 그걸 사용하지만 없으면 this.proto[‘action’] 을 찾게 됩니다. 예제의 경우 이 단계에서 찾게 되었죠.
 
 

프로토타입 체인

하지만 만약 __proto__ 에서도 못 찾게 되면 계속해서 그 안의 __proto__ 를 타고 갑니다. 즉 this.proto.proto[‘action’] 을 찾게 되는 것이죠.

이런 식으로 끝없이 __proto__ 를 추적하다 보면 어느 순간 더 이상 __proto__ 가 존재하지 않는 객체를 만나게 됩니다.

__proto__ 가 존재하지 않는 오브젝트를 prototype 으로 갖고 있는 유일한 함수는 바로 Object 함수입니다.

근데 __proto__.__proto__…. 을 키를 찾을 때 누가 가능하게 만드는 걸까요? 이게 그 유명한 프로토타입 체인이라는 자바스크립트의 키찾기 시스템입니다.

이 체인 키 찾기 시스템이 도입된 이유는

  • 클래스와 같은 정의된 메모리 공간이 따로 존재하지 않기 때문에
  • 공유데이터를 보관할 방법이 없어
  • 특정 프로토콜 규격을 따르는 일련의 해시맵끼리 데이터를 공유할 목적으로

디자인 된 것입니다. 브랜든 할베의 묘책입니다…만 이 묘책은 양면성을 갖고 있습니다(모든 묘책은 그런 법입니다. 등가교환의 법칙 때문에 ^^)

말하자면 프로토타입 체인 시스템은 고전적인 데이터구조론으로 보자면 노드가 해시맵인 일종의 링크드리스트로 이해할 수 있습니다. 따라서 링크드리스트의 장단점을 그대로 계승할 것도 같지만, 링크드리스트와는 달리 노드 간의 연결을 통제할 수 있는 인터페이스가 매우 제한적이라 거의 단점만 남게 됩니다.

링크드리스트 단점이라면 당연히 검색 속도입니다. 헤더노드에 가까울수록 빨리 찾아지고 멀리 있을수록 늦게 찾아집니다. 특히 해시맵이므로 노드를 하나씩 전진할 때마다 단지 다음 노드의 메모리참조 비용만 발생하는게 아니라 해시함수 실행 비용도 발생합니다.

깊은 참조를 형성하여 temp.proto.proto.proto.toString() 정도에서 문자열을 만들 정도면 이미 해시함수 만해도 4번 호출된 상태입니다. V8 에 적용된 해시컬리전으로 해시함수만 떼어서 성능테스트를 해봐도 모든 자바스크립트 기저단에서 발생한다고 생각해보면 이 비용은 결코 싸지 않습니다.

어쨌든 브랜든 할베가 짐 클라크에게 쪼임을 당해서 이걸 만들던 시점을 생각해보면 486DX가 최신 기종인 시절입니다. 게다가 자바스크립트는 이미 브라우저가 구동된 상태 위에서 우선순위와 메모리 할당순위도 낮은 모듈이었습니다. 저장매체가 비싸 메모리 제약이 거의 게임기 수준이었던걸 생각하면 연산 비용을 지불하고 메모리를 아끼는 방법을 택하는건 당연합니다.

현재에 와서는 거의 모든 브라우저는 이러한 링크드리스트 스타일의 프로토타입 체인 시스템을 구현하지 않습니다. 스크립트 인터페이스로서 프로토콜만 유지하는거죠(하지만 IE6,7,8은 여전히 이렇다는..=.=)

요즘 브라우저는 오브젝트를 생성하면 즉시 캐시객체를 내부에 생성하고 프로토타입 체인으로 얻어야하는 모든 키를 그 캐시객체에 미리 잡아둡니다. 체인을 통해 값을 얻는게 아니라 캐시객체에 질의하면 체인과정 없이 즉시 얻을 수 있는 거죠.

요점은 원론적인 오리지널 프로토타입 키체인 시스템에서 다중 체인에서 느려진다는 특성을 무시하고 개발하면 IE8 이하에서 심각한 성능저하가 일어난다는 것입니다.
 
 

프로토타입 체인 만들기

예를 들어 Alertprototype에 다른 함수를 new로 생성한 객체를 할당해 봅시다.

function Alert( $msg ){
	this._msg = $msg;
}
Alert.prototype = new Array();

(이게 좀 쇼크일 수도 있지만 자바스크립트적인 생각을 키우기엔 나쁘지 않은 것 같습니다 ^^)

Alert.prototype 에는 오브젝트가 와야 하는데

  1. 자바스크립트의 모든 인스턴스는 오브젝트이므로 배열로 물론 오브젝트입니다.
  2. new Array 함수의 조합으로 오브젝트를 생성했으니,
  3. 그 오브젝트의 __proto__ 에는 Array.prototype 이 들어가 있을 겁니다.

따라서 아래와 같은 게 성립합니다.

Alert.prototype.__proto__ == Array.prototype;
Alert.prototype.__proto__.join == Array.prototype.join;

이제 남은 건 원래 있던 action 을 추가하는 일입니다. 위에 말씀 드린 그대로 배열도 오브젝트이므로 그대로 키에 넣어주면 그만입니다.

function Alert( $msg ){
	this._msg = $msg;
}
Alert.prototype = new Array();

Alert.prototype.action = function(){
	alert( this._msg );
}

물론 Alert 를 통해 만든 인스턴스는 오브젝트이지 배열이 아닙니다. 배열은 오직 배열함수를 통해 만들 때만 특별하게 파서가 생성해주는 객체입니다. 하지만 그렇다고 프로토타입 체인에 의한 키가 안 잡힌 건 아닙니다. 따라서 아래와 같이 됩니다.

var a = new Alert( 'test' );

// 배열의 메서드도 존재한다!
alert( a.concat );

위의 코드에서 a.concat 을 찾아낸 방법이 바로 a.proto.proto[‘concat’] 에서 찾은 것입니다.

 
 
 

프로토타입 시스템 전반을 이용한 활용

이제 모든 오브젝트는 키를 찾을 때 그저 자신만의 키를 찾는 것이 아니라 프로토타입 체인을 통해 연결된 모든 오브젝트의 키를 찾는다는 사실을 알게 되었습니다. 여기서 중요한 점이 있는데 키에 뭔가 쓸 때는 절대로 자신에게 쓴다는 것입니다. a.proto.data 에 3이 있는 경우를 생각해 보겠습니다.

function Test(){}
Test.prototype.data = 3;

var a = new Test;

alert( a.data ); // 3 = a.__proto__.data

조회할 때야 저렇게 키체인을 뒤져서 a에 없어도 찾아옵니다. 하지만 쓰기를 할 때는 자신에게 씁니다.

a.data = 5;

즉 이 경우에 일어난 일은 a자체에 data라는 키를 잡고 5를 쓴 것이지 Test.prototype.data 에 값을 쓴게 아닙니다. 따라서 a.data 에 5를 할당 이후에도 다른 객체를 생성하면 data 키에 3이 들어있습니다.

var a = new Test;
a.data = 5;

var b = new Test;
alert( b.data ); // 3

간단히 말해

  • get 은 프로토타입체인을 통해 얻지만 set 은 오직 자기 자신에게만 쓴다,
  • 자신에게 쓰면 프로토타입체인 검색시 가장 먼저 걸리므로 우선 순위가 가장 높은 get이 된다

는 것입니다. a.data = 5 를 한 경우 사실 a.data 도 존재하고 a.proto.data 도 존재하는 상황입니다만, 체인검색의 순서상 자신의 키가 우선 걸리기 때문에 결과는 5가 되는 것이죠.
이러한 개념을 베이스로 몇 가지 프로토타입 응용을 소개하는 것으로 2부를 마칠까 합니다.
 
 

inherit

상속은 기본이죠. 클래스의 상속을 흉내내려면 어떤 함수의 prototype 에 들어갈 객체가 원하는 __proto__ 를 갖고 있으면 됩니다. 하지만 자바스크립트에서 __proto__ 를 설정하는 유일한 방법은 new 연산자입니다. 따라서 한 마디로 하자면

자식이 될 함수의 prototype new 를 통해 부모 함수의 객체를 생성 할당한다

입니다.

function Parent(){}
function Child(){}
Child.prototype = new Parent;

var a = new Child;

그럼 간단히 다음이 성립합니다.

a.__proto__ = Child.prototype = new Parent;
a.__proto__.__proto__ = (new Parent).__proto__ = Parent.prototype;

이 패턴은 흔하지만 쓸모가 있냐면 그것도 미묘합니다. 무슨 북두신권도 아니고 요즘 세상에 일자전승을 통해 해결할 수 있는 문제는 점점 줄어들고 있거든요. 하지만 공개용 인터페이스 용도 등에 사용하면 나쁘지 않습니다.

//생성시 구상메서드를 받는다.
function Character( $action ){
	this._action = $action;
}
Character.prototype.action = function(){
	//구상메서드로 처리하자
	this._action();
}
//자식 클래스 생성
function Player(){}
Player.prototype = new Character( function(){alert("Player");} );
function NPC(){}
NPC.prototype = new Character( function(){alert("NPC");} );
function Enemy(){}
Enemy.prototype = new Character( function(){alert("Enemy");} );

//Character의 action 사용
var c = [new Player, new NPC, new Enemy];
c[0].action(); // Player
c[1].action(); // NPC
c[2].action(); // Enemy

자바스크립트가 함수를 문이 아니라 값으로 처리해주는 특성은 짝퉁같은 함수형언어의 특성이 아닙니다. 단지 이것만으로도 함수가 문으로된 자바나 c++ 에서는

  • 다수의 클래스를 정의해야만 하는 상황을 제거하고
  • 즉시 함수의 형태를 정의하는 간편함으로부터
  • 기존 oop언어에서는 정의 시 명확히 나눠야하는 역할별 클래스가 없어져
  • 다양한 패턴의 중간 단계를 활용하기가 편리하게 됩니다

위의 코드만 해도 템플릿메서드패턴+스트레티지패턴 의 중간 형태입니다. 자바로 못할게 아니지만 그걸 위한 전용 클래스를 따로 정의하고 그 이후엔 그렇게 밖에 못쓰는게 큰 차이점인거죠.
함수객체의 유연함은 암묵적인 this 키 연결이 자유로워질 때 비로서 커링같은 함수형 언어처럼 활용하는게 아닌 진정한 즉 정의 클래스처럼 사용되게 됩니다.
 
 

copy

자바는 병행함수포인터를 이용해 가로로 상속구조를 확장할 수 있는 인터페이스를 제공하고 있습니다. 이것은 수직적인 체인관계를 가로로 확장하는 시스템이라 메모리상에 형별로 다중 인스턴스를 생성하는게 아니라 그저 프로토타입객체 하나만 링크로 연결하는 자바스크립트에게는 그냥은 흉내낼 수 없는 시스템입니다. 따라서 __proto__ 체인을 포기하고 인터페이스의 모든 구조를 복사해버릴 수 밖에 없습니다.

function Imp1(){}
function Imp2(){}

function Concreate(){}
//한 개 밖에 안되잖아!
Concreate.prototype = new Imp1;

그렇습니다. 일자전승! 그냥은 한게 밖에 안됩니다. 유일한 방법은 Imp1 Imp2 의 키를 Concreate 에 복사하는 것입니다.

for( key in Imp1 ) Concreate.prototype[key] = Imp1[key];
for( key in Imp2 ) Concreate.prototype[key] = Imp2[key];

그래도 위로라면 인스턴스 수준이 아니라 함수의 프로토타입 수준에서 복사했으니 그리 메모리 중복비용이 크지는 않다는 정도입니다 ^^;
 
 

none static

여기 나오는 패턴들의 이름은 순전히 제 맘대로 붙인 겁니다. 어디가서 공식 용어로 사용하지 마세용 ^^;;;

자바스크립트는 클래스가 없으니 당연히 모든 메서드는 정적입니다만, 프로토타입체인에 연결되지 않는다는 의미로 개발자들 사이에서 자주 스태틱이란 용어를 사용하곤 합니다. 저도 살짝 편승해본건데 일반적인 정적함수는 다음과 같이 소개되곤 합니다.

function Test(){}

//프로토타입에 정의하면 인스턴스용
Test.prototype.method = function(){};

//함수객체의 키에 정의하면 스태틱
Test.staticMethod = function(){};

두 개의 함수는 실제 아래와 같이 구별되어 사용되게 됩니다.

var a = new Test;

//인스턴스용
a.method();

//스태틱은 a와 같은 인스턴스와 무관하게 사용
Test.staticMethod();

하지만 Test.staticMethod() 를 곰곰히 생각해보면 존재하면 안된다는 결론에 이릅니다.

  • Test 는 컨테이너일뿐 함수일 필요가 없으므로 그저 {} 면 된다
  • Test.staticMethod Test 는 관계가 없다
  • 인스턴스용 메서드만 사용할 staticMethod가 필요하면 모듈로 정의해야 한다
  • 일반 유틸리티라면 유틸리티를 위한 이름공간을 새로 생성하여 만드는게 정상

이래저래 생각해봐도 저렇게 써야할 이유는 원론적으로는 없는 거죠. 하지만 현실에서는 다음과 같은 이유가 있습니다.

  • 외부 배포나 타 개발자에게 전달할 때 다수의 클래스를 전달하는 것보다 하나의 클래스로 전달하고 싶다
  • 이름공간에 다수의 객체를 올리고 싶지 않다
  • 어떤 경우 정적인 역할도 수행하면서 인스턴스의 기능도 동시에 수행하는 1인 2역 클래스로 정리하고 싶은 객체도 있다

어쨌든 이 패턴의 이름은 none static이므로 정적함수를 정의못하게 하는게 목적입니다. 방법은 간단합니다.

function Test(){}
//자신을 자신의 프로토타입으로 잡는다.
Test.prototype = Test;

엥 정말? 네, 문제없습니다. 함수는 근본적으로 Function 이라는 함수로부터 생성된 오브젝트입니다. prototype 에는 오브젝트가 오면 그만이므로 Test 를 넣으면 됩니다. 그럼 순환참조가!!!
네, 순환참조 됩니다. 근데 머..문제없습니다 ^^

Test.prototype == Test
Test.prototype.prototype = Test = Test.prototype

언틋보면 이 순환참조관계는 마치 c++의 죽음의 다이아몬드 상속처럼 보이는데?!! 괜찮습니다. 왜냐면 프로토타입 체인은 prototype 이란 속성을 찾는 게 아니라, __proto__를 찾는 것이고 여기에는 그 객체의 생성 시 사용된 함수가 들어있습니다.

var a = new Test;
a.__proto__ = Test.prototype
a.__proto__.__proto__ = Test.prototype.__proto__

이렇게 됩니다. 따라서 prototype 을 순환하지 않게 됩니다. 그럼 Test__proto__ 엔 뭐가 들어있나요?

Function 이 들어있습니다(대문자!)

Function.prototype에는?

Object 가 들어있습니다. 정상적인 체인에 수렴하게 됩니다(어지럽나요? 빙글빙글 도세요 ^^)

자 암튼 이해에 도달하셨으면 많은 프레임웍에서 사용하는 fn이란 단축표현이 필요없어집니다.

function Test(){}
var fn = Test.prototype;
fn.method1 = function(){};
fn.method2 = function(){};

var a = new Test;
a.method1();
a.method2();

이런식의 표현이 많이 나오는데 none static구조에서는 그냥 함수에 정의하는 것으로 충분합니다.

function Test(){}
Test.prototype = Test;
Test.method1 = function(){};
Test.method2 = function(){};

var a = new Test;
a.method1();
a.method2();

 
 

그 외의 활용

에, 이 외에도 숱한 변형 패턴들이 존재합니다만 이미 많은 곳에서 소개된건 과감하게 생략했습니다(너무 많이 했나!) 상속과 복사를 통한 상속은 너무 기본이라 적었고 정적금지는 걍 글쓰다가 생각나서..=.=;

이 동네는 인사이트에서 역서로 낸 자바스크립트 코딩기법과 핵심패턴에 상당히 수록 되어있습니다. 6장 코드 재사용 패턴에 프로토타입의 다양한 사용법이 한가득 나오니 걍 다 외우세요. 입문자 분들이 진정으로 이 이상한 프로토타입시스템을 자유롭게 사용하고 응용하려면 그 6장을 이해하는 정도가 아니라 외워서 아무곳이나 푹찌르면 줄줄 나올 정도로 몸에 익힌 뒤 입니다.

이 책의 6장에는 제가 귀찮아서 생략한 생성자 관련 내용도 나오기 때문에 자바스크립트를 부득불 oop로 쓰고 싶은 자바, c++개발자 여러분들은 꼭 읽으시길 권장합니다.

그럼 브랜든할베의 지옥에서 좀 자유롭게 언어를 만질 수 있게 됩니다.

  • 블로그에 있던 제 불평불만에 인사이트 관계자 분이 직접 댓글을 남기셔서 황송한 맘에 살짝 광고 해드린 걸로 퉁치기라고 광고성이 짙어져버렸습니다 ㅎㅎ
     
     
     

결론

  1. 자바스크립트는 클래스도 상속도 없지만 프로토타입을 통한 공유가 가능하다.
  2. new 구문을 사용하면 함수의 프로토타입을 __proto__ 에 참조로 잡아 함수의 프로토타입에 정의한 함수나 변수를 사용할 수 있다.
  3. 하지만 이러한 키를 찾는 과정은 전부 실행 시점에 일어난다.
  4. 따라서 프로토타입 체인을 깊이 사용하면 하나의 키를 식별하기 위해 수 많은 __proto__ 를 찾아야 하므로 성능이 낮아진다.
  5. IE8 이하의 경우 프로토타입 체인이 한 단계 깊어질 때마다 키를 찾는 비용이 단조 증가한다.
  6. 근본적으로 직접 오브젝트에 해당 함수를 키로 잡지 않고 프로토타입을 사용하면 그만큼 인스턴스의 키에 할당할 메모리를 절약한다.
  7. 반대로 키를 찾는 과정에 비용을 더 사용하게 되므로 메모리를 절약하고 속도가 느려지는 구조다.
  8. 개발자는 new 를 통해 프로토타입 시스템을 사용할지 직접 오브젝트에 키로 함수를 지정할지 속도와 메모리 사이에서 결정해야 한다.