2017. 1. 19. 19:08ㆍDev/javascript
객체를 바르게 만들기
자바스크립트 패턴과 테스트 3장 요약
원시형
ECMAScript5 기준 : 문자열(String), 숫자(Number), 불(Boolean), null, undefined
ECMAScript6 기준 : ECMAScript5 + 심볼(Symbol)
객체 리터럴
다음과 같이 선언한 객체를 말한다.
객체 리터럴
{ name : 'koko', genus: 'gorilla', genius: 'sign language' }
객체 리터럴을 선언하는 두 가지 방법
1. 단순 객체 리터럴(bare object literal)
var koko = { name : 'koko', genus: 'gorilla', genius: 'sign language' }
2. 함수 반환값 객체 리터럴
var amazeTheWorld = function(){
return { name : 'koko', genus: 'gorilla', genius: 'sign language' };
}
var koko = amazeTheWorld();
함수 반환 객체 릴터럴은 의존성 주입에 용의하며 리터럴 생성시 검증에도 한결 편하다.
모듈 패턴
모듈 패턴은 자바스크립트의 가장 명망 높은 패턴 중 하나다. 데이터 감춤이 주목적인 함수가 모듈 API를 이루는 객체를 반환하게 한다. 이 패턴은 두 가지 유형, 즉 임의로 함수를 호출하여 생성하는 모듈, 선언과 동시에 실행하는 함수에 기반을 둔 모듈이 있다.
임의 모듈 생성
// 해당 애플리케이션에서만 사용할 수 있는 모든 객체(모듈)를 담아 넣은 전역 객체를 선언하여 이름 공간처럼 활용한다.
var MyApp = MyApp || {};
// 애플리케이션 이름공간에 속한 모듈
// 이 함수는 animalMaker라는 다른 함수에 의존하며 animalMaker는 주입 가능하다.
MyApp.wildifePreserveSimulator = function(animalMaker) {
// 프라이빗 변수
var animals [];
// API를 반환
return {
addAnimal: function(species, sex) {
animals.push(animalMaker.make(species, sex));
},
getAnimalCount: function() {
return animals.length;
}
};
}
모듈은 다음과 같이 사용된다.
var preserve = MyApp.wildifePreserveSimulator(realAnimalMaker);
preserve.addAnimal(gorilla, female);
즉시 생성 모듈 생성
var MyApp = MyApp || {};
MyApp.wildifePreserveSimulator = function(animalMaker) {
var animals [];
return {
addAnimal: function(species, sex) {
animals.push(animalMaker.make(species, sex));
},
getAnimalCount: function() {
return animals.length;
}
};
}()); // <- 즉시 실행한다.
싱글톤은 이렇게 사용한다
MyApp.wildifePreserveSimulator.addAnimal(realAnimalMaker, gorilla, female);
즉시 생성 모듈은 싱글톤 인스턴스가 된다.
만약 외부함수를 통해서 즉시 실행 모듈을 생성한다고 할 때 의존성이 있는 외부함수를 가져오지 못하면 외부 함수에 주입할 수 없다. 이런 점은 불편하다. 싱글톤이 꼭 필요하다면 임의 모듈 패턴으로 모듈을 코딩하고 해당 모듈을 요청할 때마다 의존성 주입 프레임워크에서 같은 인스턴스를 제공하는 편이 의존성 주입 측면에서 더 낫다. 앵귤러JS가 '서비스'싱글톤을 내어주는 방식도 이와 같다.
모듈 생성의 원칙
- 단일 책임 원칙을 잊지 말고 한 모듈에 한 가지 일만 시키자. 그래야 결속력 강하고 다루기 쉬운 아담한 API를 작성하게 된다.
- 모듈 자신이 쓸 객체가 필요하다면 의존성 주입 형태로(직접 또는 팩토리 주입 형태로) 이 객체를 제공하는 방안을 고려하라
- 다른 객체 로직을 확장하는 모듈은 해당 로직의 의도가 바뀌지 않도록 분명히 밝혀라(리스코프 치환 원칙)
객체 프로토타입과 프로토타입 상속
자바스크립트 객체는 생성 메커니즘과 무관하게 프로토타입 객체로 연결되어 프로퍼티를 상속한다.
기본 객체 프로토타입
var chimp = {
hasThumbs: true,
swing: function(){
return 'prototype is cool';
}
};
위 코드를 작성하고 아래의 코드를 실행해보자
newObj.toString();
undefined 함수 에러 가 나지 않는다.
실행되는 순간 newObj에 직접구현된 toString()을 찾지만 없다면 Object.prototype에서 찾아보고 실행시킨다. 만들어진 모든 객체는 Object.prototype을 지니고 있다. 만약 toString 이 재정의 되어 있다면 Object.prototype 가 아닌 newObj에 있는 toString()을 실행시킨다.
프로토타입 상속
var ape = {
hasThumbs: true,
hasTail: false,
swing: function(){
return '매달리기';
}
};
var chimp = Object.create(ape);
var bonobo = Object.create(ape);
bonobo.habitat = '중앙 아프리카';
console.log(bonobo.habitat); // '중앙 아프리카' (bonobo 프로토타입)
console.log(bonobo.hasTail); // false (ape 프로토타입)
console.log(chimp.swing()); // 매달리기 (ape 프로토타입)
프로토타입 체인
프로토타입 체인이라는 다층 프로토타입을 이용하면 여러 계층의 상속을 구현할 수 있다.
var primate = {
stereoscopicVision: true
};
var ape = Object.create(primate);
ape.hasTail = false;
var chimp = Object.create(ape);
console.log(chimp.hasTail); // false (ape 프로토타입)
console.log(chimp.stereoscopicVision); //true (primate 프로토타입)
chimp.stereoscopicVision은 이 객체의 고유 프로퍼티가 아닌 까닭에 자바스크립트 엔진은 chimp의 프로토타입 체인을 따라 ape를 넘어 primate에서 해당 프로퍼티를 발견한다. 모두 뒤져도 없으면 undefined를 반환한다. 너무 깊숙이 프로토 타입 체인을 찾게 하면 성능상 좋을 게 없으니 될 수 있으면 너무 깊이 체인을 쓰지 않는 편이 좋다.
new 객체 생성
다음은 함수의 new 객체 생성 패턴으로 각각의 객체 인스턴스를 생성하는 코드다.
function Marsupial(name, nocturnal) {
this.name = name;
this.isNocturnal = nocturnal;
}
var maverick = new Marsupial('매버릭', true);
var slider = new Marsupial('슬라이더', false);
console.log(maverick.isNocturnal); // true
console.log(maverick.name); // "매버릭"
console.log(slider.isNocturnal); // false
console.log(slider.name); // "슬라이더"
instanceof 연산자로 강제
function Marsupial(name, nocturnal) {
if (!(this instanceof Marsupial)) {
throw new Error("이 객체는 new를 사용하여 생성해야 합니다");
}
this.name = name;
this.isNocturnal = nocturnal;
}
new를 자동으로 삽입하여 인스턴스를 생성
function Marsupial(name, nocturnal) {
if (!(this instanceof Marsupial)) {
return new Marsupial(name, nocturnal);
}
this.name = name;
this.isNocturnal = nocturnal;
}
new 객체 생성 패턴을 이용하면 정의부 하나로 여러 인스턴스가 함께 사용할 함수 프로퍼티를 생성할 수 있다.
function Marsupial(name, nocturnal) {
if (!(this instanceof Marsupial)) {
throw new Error("이 객체는 new를 사용하여 생성해야 합니다");
}
this.name = name;
this.isNocturnal = nocturnal;
// 각 객체 인스턴스는 자신만의 isAwake 사본을 가진다
this.isAwake = function(isNight) {
return isNight === this.isNocturnal;
}
}
var maverick = new Marsupial('매버릭', true);
var slider = new Marsupial('슬라이더', false);
var isNightTime = true;
console.log(maverick.isAwake(isNightTime)); // true
console.log(slider.isAwake(isNightTime)); // false
// 각 객체는 자신의 isAwake 함수를 가진다
console.log(maverick.isAwake === slider.isAwake); // false
생성자 함수 프로토타입에 함수를 추가(위의 코드와 다른점을 눈여겨 보아야 한다)
function Marsupial(name, nocturnal) {
if (!(this instanceof Marsupial)) {
throw new Error("이 객체는 new를 사용하여 생성해야 합니다");
}
this.name = name;
this.isNocturnal = nocturnal;
}
// 각 객체 isAwake 사본하나를 공유한다.
Marsupial.prototype.isAwake = function(isNight) {
return isNight === this.isNocturnal;
}
var maverick = new Marsupial('매버릭', true);
var slider = new Marsupial('슬라이더', false);
var isNightTime = true;
console.log(maverick.isAwake(isNightTime)); // true
console.log(slider.isAwake(isNightTime)); // false
// 객체들은 isAwake의 단일 인스턴스를 공유한다
console.log(maverick.isAwake === slider.isAwake); // true
생성자 함수 프로토타입을 이용하면 객체 인스턴스가 각각 isAwake함수 사본을 생성하여 들고 있는 코드보다 90%이상 실행이 빠르다.
new로 생성한 객체마다 각각 독립된 property를 정의하고 싶다면 함수 안에 정의.
new로 생성한 객체 모두가 공유하는 property를 정의하고 싶다면 prototype을 이용하여 정의.
클래스 상속
자바스크립트는 클래스가 없으므로 C#, C++처럼 클래스 상속을 할 수는 없지만, 프로토타입 상속으로 어느 정도 흉내를 낼 수는 있다.
고전적 상속 흉내내기
function Marsupial(name, nocturnal) {
if (!(this instanceof Marsupial)) {
throw new Error("이 객체는 new를 사용하여 생성해야 합니다");
}
this.name = name;
this.isNocturnal = nocturnal;
}
Marsupial.prototype.isAwake = function(isNight) {
return isNight == this.isNocturnal;
};
function Kangaroo(name) {
if (!(this instanceof Kangaroo)) {
throw new Error("이 객체는 new를 사용하여 생성해야 합니다");
}
this.name = name;
this.isNocturnal = false;
}
Kangaroo.prototype = new Marsupial();
Kangaroo.prototype.hop = function() {
return this.name + "가 껑충 뛰었어요!";
};
var jester = new Kangaroo('제스터');
console.log(jester.name);
var isNightTime = false;
console.log(jester.isAwake(isNightTime)); // true
console.log(jester.hop()); // '제스터가 껑충 뛰었어요!'
console.log(jester instanceof Kangaroo); // true
console.log(jester instanceof Marsupial); // true
위의 고전적 상속은 코드 반복과 메모리 점유라는 문제가 있다.
console.log(jester); 로 확인해보면 Marsupial 인스터스는 물론 kangaroo 인스턴스가 각각 name, inNocturnal 프로퍼티를 들고 다니는 것을 볼 수 있다.
함수형 상속
함수형 상속을 하면 데이터를 숨긴 채 접근을 다스릴 수 있다.
모듈 패턴 역시 고전적 상속 흉내 내기에서 생성자 로직 중복을 들어냈던 식으로 깔끔하게 상속을 지원한다.
function Marsupial(name, nocturnal) {
if (!(this instanceof Marsupial)) {
throw new Error("이 객체는 new를 사용하여 생성해야 합니다");
}
this.name = name;
this.isNocturnal = nocturnal;
}
// 각 객체 인스턴스는 자신만의 isAwake 사본을 가진다
Marsupial.prototype.isAwake = function(isNight) {
return isNight === this.isNocturnal;
}
var maverick = new Marsupial('매버릭', true);
var slider = new Marsupial('슬라이더', false);
var isNightTime = true;
console.log(maverick.isAwake(isNightTime)); // true
console.log(slider.isAwake(isNightTime)); // false
// 객체들은 isAwake의 단일 인스턴스를 공유한다
console.log(maverick.isAwake === slider.isAwake); // true
모듈을 이용한 함수형 상속은 고전적 상속 흉내 내기와 달리 AnimalKingdom.marsupial의 생성 로직을 AnimalKingdom.kangaroo에서 재탕할 필요가 없다. 직접 사용하기 떄문이다.
멍키 패칭
멍키 패칭은 추가 프로퍼티를 객체에 붙이는 것이다.
var MyApp = MyApp || {};
MyApp.Hand = function(){
this.dataAboutHand = {}; // etc.
};
MyApp.Hand.prototype.arrangeAndMove = function(sign) {
this.dataAboutHand = '새로운 수화 동작';
};
MyApp.Human = function(handFactory) {
this.hands = [ handFactory(), handFactory() ];
};
MyApp.Human.prototype.useSignLanguage = function(message) {
var sign = {};
// 메시지를 sign에 인코딩한다.
this.hands.forEach( function(hand) {
hand.arrangeAndMove(sign);
});
return '손을 움직여 수화하고 있어. 무슨 말인지 알겠니?';
};
MyApp.Gorilla = function(handFactory){
this.hands = [handFactory(), handFactory()];
};
MyApp.TeachSignLanguageTokoko = (function() {
var handFactory = function() {
return new MyApp.Hand()
};
//(빈자의 의존성 주입)
var trainer = new MyApp.Human(handFactory);
var koko = new MyApp.Gorilla(handFactory);
koko.useSignLanguage = trainer.useSignLanguage;
// 실행 결과: 손을 움직여 수화하고 있어. 무슨 말인지 알겠니?'
console.log(koko.useSignLanguage('Hello!'));
}());
다음 줄 끝에서 멍키 패칭이 일어난다.
koko.useSignLanguage = trainer.useSignLanguage;
조련사(trainer)의 수화(sign language) 능력을 코코(koko)에게 패치한다. 코코에게 손(Hands)이 있기에 가능한 일이다. useSignLanguage는 Human에서 비롯되었지만. 이 함수 앞에 점을 붙여 호출하면(koko.useSignLanguage) 사람(Human)이 아닌 코코의 손을 움직인다. 단계별로 알아보자.
- koko.useSignLanguage('Hello')를 호춯한다.
- 멍키 패칭을 했으니 MyApp.Human.prototype.useSignLanguage가 실행된다.
- 이 함수는 this.hands에 접근한다.
- 여기서 this는 useSignLanguage를 호출한 객체, 즉 MyApp.Gorilla 객체(koko)다.
빌린 함수에 다른 요건이 추가될 가능성은 항상 있다. 따라서 패치를 관장하는 빌려주는 객체는 빌리는 객체가 요건을 충족하는지 알아보게 하는 것이 가장 좋은 멍키 패칭 방법이다. 그에 따른 추가 코드는 다음과 같다.
MyApp.Human.prototype.endowSigning = function(receivingObject) {
if (typeof receivingObject.getHandCount === 'function'
&& receivingObject.getHandCount() >= 2) {
receivingObject.useSignLanguage = this.useSignLanguage;
} else {
throw new Error("미안하지만 너에게 수화를 가르쳐줄 수는 없겠어.");
}
};
물론 빌리는 객체는 빌려주는 객체의 질문에 반드시 대답해야 한다.
MyApp.Gorilla.prototype.getHandCount = function() {
return this.hands.length;
};
이제 사람이 고릴라에게 수화 능력을 재능 기부할 차례다.
trainer.endowSigning(koko);
추가된 코드를 포함하는 전체 코드
var MyApp = MyApp || {};
MyApp.Hand = function(){
this.dataAboutHand = {}; // etc.
};
MyApp.Hand.prototype.arrangeAndMove = function(sign) {
this.dataAboutHand = '새로운 수화 동작';
};
MyApp.Human = function(handFactory) {
this.hands = [handFactory(),handFactory()];
};
MyApp.Human.prototype.useSignLanguage = function() {
var sign = {};
// 메시지를 sign에 인코딩한다.
this.hands.forEach( function(hand) {
hand.arrangeAndMove(sign);
});
return '손을 움직여 수화하고 있어. 무슨 말인지 알겠니?';
};
MyApp.Human.prototype.endowSigning = function(receivingObject) {
if (typeof receivingObject.getHandCount === 'function'
&& receivingObject.getHandCount() >= 2) {
receivingObject.useSignLanguage = this.useSignLanguage;
} else {
throw new Error("미안하지만 너에게 수화를 가르쳐줄 수는 없겠어.");
}
};
MyApp.Gorilla = function(handFactory){
this.hands = [handFactory(), handFactory()];
};
MyApp.Gorilla.prototype.getHandCount = function() {
return this.hands.length;
};
MyApp.TeachSignLanguageTokoko = (function() {
var handFactory = function() {
return new MyApp.Hand()
};
//(빈자의 의존성 주입)
var trainer = new MyApp.Human(handFactory);
var koko = new MyApp.Gorilla(handFactory);
trainer.endowSigning(koko);
// 실행 결과: 손을 움직여 수화하고 있어. 무슨 말인지 알겠니?'
console.log(koko.useSignLanguage());
}());
이런 식으로 한 객체의 기능 다발 전체를 다른 객체로 패치할 수도 있다. 정통 개발자들은 이 기능 다발이 곧 인터페이스 구현체가 아닌가 싶을 텐데, 인터페이스뿐만 아니라 코드도 함꼐 구현한 터라 사실 다중 상속(multiple inheritance)에 더 가깝다.
'Dev > javascript' 카테고리의 다른 글
Array.prototype.slice.call(arguments) 에 대하여 (2) | 2017.06.25 |
---|---|
패턴 (0) | 2017.06.04 |
자바스크립트 도구 다루기 (0) | 2017.01.19 |
자바스크립트 특징을 보여주는 코드 (0) | 2017.01.18 |
javascript call() (0) | 2017.01.18 |