3.9使用Stamps进行原型继承
对象在JavaScript中,有各式各样灵活的特性,相比之下通过Object.create()方法所构建出的对象,感觉就像被“阉割”了一样。开发者总是需要自己去编写额外的代码来实现诸如让享元对象支持数据隐蔽特性这样的特性,所以当你需要将多个对象上的功能做组合时,一般情况下你会感觉非常无力。
现今多数JavaScript类库中提供了一套与类继承概念相似的对象复用机制,基于原型继承的JavaScript类库占比还是太少,所以就更别提什么业界标准了。既然少量语法糖就能够在JavaScript中模拟类的概念,那么为什么不能构建一个囊括了JavaScript所有强大特性的原型继承系统呢?
当聊起JavaScript中的对象构建时,我们不禁首先会问自己,在JavaScript中,对象所具有的最为重要的特性有哪些?
· 原型代理
· 实例状态
· 封装性
Stamps是一个工厂函数,它拥有一组公有属性与方法,这些属性方法用来定义所构建对象的原型代理、默认的实例属性以及为封装私有属性所需的函数。在构建对象方面,Stamps提供了3种不同的继承方式。
· 原型代理:原型继承
· 实例状态:属性混入
· 封装性:闭包式继承
Stampit(https://github.com/dilvie/stampit)是为本书而写的类库,用来向读者展示如何利用JavaScript自带的语言特性来简化原型继承,它向外界仅暴露一个入口函数,其函数签名如下:
stampit(methods, state, enclose);
让我们看看如何使用Stampit来构建对象:
var testObj = stampit(
// methods
{
delegateMethod: function delegateMethod() {
return 'shared property';
}
},
// state
{
instanceProp: 'instance property'
},
// enclose
function () {
var privateProp = 'private property';
this.getPrivate = function getPrivate() {
return privateProp;
}
}).create();
test('Stampit with params', function () {
equal(testObj.delegateMethod(), 'shared property',
'delegate methods should be reachable');
ok(Object.getPrototypeOf(testObj).delegateMethod,
'delegate methods should be stored on the ' +
'delegate prototype');
equal(testObj.instanceProp, 'instance property',
'state should be reachable.');
ok(testObj.hasOwnProperty('instanceProp'),
'state should be instance safe.');
equal(testObj.hasOwnProperty('privateProp'), false,
'should hide private properties');
equal(testObj.getPrivate(), 'private property',
'should support privileged methods');
});
在上例中,我们在stampit实例上调用create()方法,它返回testObj的实例。代码下方的测试用例显示,testObj实例已经拥有了诸多原型继承体系下的杀手级特性,你已经无需再花费大量精力去实现自己的工厂函数了。
stampit实例支持方法链式调用,上例中的对象也可以被这样创建:
var testObj = stampit().methods({
delegateMethod: function delegateMethod() {
return 'shared property';
}
})
.state({
instanceProp: 'instance property'
})
.enclose(function () {
var privateProp = 'private property';
this.getPrivate = function getPrivate() {
return privateProp;
}
})
.create();
由Object.create()构建出的对象会默认使用原型上定义的方法,这些方法被所有实例所共享,这样做在很大程度上节省了内存开销。同理,如果你在程序运行期间修改了原型中的方法,所有实例均会受到波及,如下例:
var stamp = stampit().methods({
delegateMethod: function delegateMethod() {
return 'shared property';
}
}),
obj1 = stamp(),
obj2 = stamp();
Object.getPrototypeOf(obj1).delegateMethod =
function () {
return 'altered';
};
test('Prototype mutation', function () {
equal(obj2.delegateMethod(), 'altered',
'Instances share the delegate prototype.');
});
state()方法采用了“属性混入”的方式,它将传入的对象视为实例的原型代理,并将该对象的每项属性拷贝后添加至新实例中。由于是拷贝而非直接引用,所以你可以放心大胆地对属性做后期修改。所有stampit实例在做对象构建时,均接受一个可选的字典对象,字典对象中的属性会被混入新实例中,这让对象实例化这一过程变得更为简单。
var person = stampit().state({name: ''}),
jimi = person({name: 'Jimi Hendrix'});
test('Initialization', function () {
equal(jimi.name, 'Jimi Hendrix',
'Object should be initialized.');
});
enclose()方法则采用了“闭包式继承”的方式, 你可以给它传入函数,这些函数彼此独立并且各自拥有闭包作用域,从而可以确保数据的隐蔽性与唯一性。此外,如果在函数中存在多个同名特权方法的定义,那么后一项始终会有限覆盖前一项。在stampit实例中enclose()方法可以被调用多次,所传入的函数会在对象构建时被“激活”。
在初始化一个对象时,有时并不需要将参数一次性全部传入,所以我一直建议将对象的实例化与初始化解耦,引入读写方法(Getter/Setter)可以解决这个问题。
var person = stampit().enclose(function () {
var firstName = '',
lastName = '';
this.getName = function getName() {
return firstName + ' ' + lastName;
};
this.setName = function setName(options) {
firstName = options.firstName || '';
lastName = options.lastName || '';
return this;
};
}),
jimi = person().setName({
firstName: 'Jimi',
lastName: 'Hendrix'
});
test('Init method', function () {
equal(jimi.getName(), 'Jimi Hendrix',
'Object should be initialized.');
});
前面我们介绍了用stampit进行对象构建,其实这仅仅是JavaScript面向对象特性的冰山一角。接下来我们介绍的内容你很难在现有的JavaScript流行类库中寻觅到,甚至连ES6规范中都没有相关定义。
首先,看如何使用闭包来封装私有数据:
var a = stampit().enclose(function () {
var a = 'a';
this.getA = function () {
return a;
};
});
a().getA(); // 'a'
stampit实例使用函数作用域来封装私有数据,请注意读写方法(Getter/Setter)需定义在函数内部,才可以访问到闭包中的变量,这一规则同样适用于JavaScript中的所有特权函数。
来看另一个例子:
var b = stampit().enclose(function () {
var a = 'b';
this.getB = function () {
return a;
};
});
b().getB(); // 'b'
这个“拼写错误”是有意为之的,它是为了向你展示实例a与实例b各自封装的同名私有变量不会对彼此的使用造成影响,而且这么做的有趣之处在于:
var c = stampit.compose(a, b),
foo = c();
foo.getA(); // 'a'
foo.getB(); // 'b'
stampit的静态方法compose()让你能够从多个stampit实例中做原型继承。上述示例演示了使用compose()方法来实现多重继承,我们看到所继承的属性中甚至连私有数据都有囊括,而现今的类继承体系中是做不到这点的。
stampit实例有一个名为fixed的特别属性,它存储着所有methods(实例方法)、state(属性)与enclose(包裹函数)的对象原型。在实例化期间,state对象的所有属性被拷贝至实例中,确保了实例属性操作的安全性;methods对象则作为实例的原型代理来使用,从而使得多个实例间可以共享一套方法;enclose对象将所有函数当作闭包来使用,实现了对私有数据的访问控制。
compose()方法的用途与$.extend()方法很接近,不过它并不是像$.extend()方法那样使用外来对象的属性做扩展,而是借助了stampit实例的fixed属性,compose()方法先是将来自多个stampit实例下的fixed属性做合并,随后向外界返回一个新stampit实例。在对同名属性进行合并时,compose()会优先使用顺序靠后的属性,在这一点上,它与$.extend(), _.extend()等方法的合并策略是一致的。