本节书摘来华章计算机出版社《JavaScript应用程序设计》一书中的第3章,第3.8节,作者:Eric Elliott 更多章节内容可以访问云栖社区“华章计算机”公众号查看。
3.8 工厂函数
使用对象字面量带来的便捷是显而易见的,不过它们无法封装私有数据。封装的概念之所以在编程中具有价值,是因为它将程序内部的实现细节对使用者做了隐藏。回忆一下“四人帮”在面向对象设计模式一书中首章的描述,“面向接口编程,而不是面向实现编程”,封装将这一编码原则在代码中贯彻,即对使用者隐藏实现细节。
不过,经过前面几节的介绍,你已经对构造函数的弊病有所了解,并知晓如何去规避。下面介绍一种构造函数的替代方案:工厂函数。
工厂函数被用来构建并实例化对象,使用它的目的在于将对象构建的细节从对象使用的过程中抽象出来,在面向对象的程序设计中,工厂函数的使用范围非常广。
回到之前单例模式的例子,将单例对象通过方法调用封装起来是非常实用的,你可以将单例对象存放在一个私有变量中,随后通过闭包来获取它的引用。
function factory() {
var highlander = {
name: 'MacLeod'
};
return {
get: function get() {
return highlander;
}
};
}
test('Singleton', function () {
var singleton = factory();
hero = singleton.get(),
hero2 = singleton.get();
hero.sword = 'Katana';
// Since hero2.sword exists, you know it's the same
// object.
ok(hero2.sword, 'There can be only one.');
});
使用相同的方法为car类添加停车与刹车功能:
var car = function car(color, direction, mph) {
var isParkingBrakeOn = false;
return {
color: color || 'pink',
direction: direction || 0,
mph: mph || 0,
gas: function gas(amount) {
amount = amount || 10;
this.mph += amount;
return this;
},
brake: function brake(amount) {
amount = amount || 10;
this.mph = ((this.mph - amount) < 0) ? 0
: this.mph - amount;
return this;
},
toggleParkingBrake: function toggleParkingBrake() {
isParkingBrakeOn = !isParkingBrakeOn;
return this;
},
isParked: function isParked() {
return isParkingBrakeOn;
}
};
},
myCar = car('orange', 0, 5);
test('Factory with private variable.', function () {
ok(myCar.color, 'Has a color');
equal(myCar.gas().mph, 15,
'.accelerate() should add 10mph.'
);
equal(myCar.brake(5).mph, 10,
'.brake(5) should subtract 5mph.'
);
ok(myCar.toggleParkingBrake().isParked(),
'.toggleParkingBrake() toggles on.');
ok(!myCar.toggleParkingBrake().isParked(),
'.toggleParkingBrake() toggles off.');
});
与构造函数的效果一样,你将私有数据封装在了闭包中,现在唯有使用特权方法.toggleParkingBrake()才可以控制刹车杆状态。
与构造函数不同的是,你无需在工厂函数前追加new关键字(或无需担心忘记new关键字时,属性与方法的赋值会污染至全局对象)。
当然,在这里你完全可以使用原型来提升代码执行效率。
var carPrototype = {
gas: function gas(amount) {
amount = amount || 10;
this.mph += amount;
return this;
},
brake: function brake(amount) {
amount = amount || 10;
this.mph = ((this.mph - amount) < 0)? 0
: this.mph - amount;
return this;
},
color: 'pink',
direction: 0,
mph: 0
},
car = function car(options) {
return extend(Object.create(carPrototype), options);
},
myCar = car({
color: 'red'
});
test('Flyweight factory with cloning', function () {
ok(Object.getPrototypeOf(myCar).gas,
'Prototype methods are shared.'
);
});
现在工厂函数本身的代码已被精简至一行,并使用对象字典options作为其参数列表,这样一来你便可以配置那些你想要覆盖的属性。
利用原型本身所具有的特性,你完全可以在程序运行期间,对原型进行任意的属性替换操作,这里,我们使用之前所定义的carPrototype原型对象:
test('Flyweight factory with cloning', function () {
// Swap out some prototype defaults:
extend(carPrototype, {
name: 'Porsche',
color: 'black',
mph: 220
});
equal(myCar.name, 'Porsche',
'Instance inherits the new name.'
);
equal(myCar.color, 'red',
'No instance properties will be impacted.'
);
});
注意: 最好不要将对象或者数组类型的属性放置在原型上托管,万一它们在实例层面上使用,你麻烦就大了。针对这种引用类型的属性,建议在工厂函数中为每个实例单独创建一份拷贝。