在基于类的语言中,对象是类的实例,并且类可以从另一个类继承。
JavaScript
是一门基于原型的语言,这意味着对象直接从其他对象继承。
伪类(Pseudoclassical)
JavaScript 原型机制存在诸多矛盾。它不直接让对象从其他对象继承,反而插入了一个多余的间接层:通过构造器函数产生对象。
当一个函数对象被创建时,Function
构造器产生的函数对象会运行类似这样的一些代码:
this.prototype = {constructor:this};
新函数被赋予一个 prototype
属性,它的值是一个包含 constructor
属性且属性值为该新函数的对象。这个 prototype
对象是存放继承特征的地方。因为 JavaScript 没有提供一种方法去确定哪个函数是打算用来做构造器的,所以每个函数都会得到一个 prototype
对象。
当采用构造器调用模式,即用 new
前缀去调用一个函数时,函数执行的方式会被修改。如果 new
运算符是一个方法而不是一个运算符,它可能会像这样执行:
//先前添加的 method 函数
Function.prototype.method = function (name,func) {
this.prototype[name] = func;
return this;
};
Function.method('new',function () {
//创建一个新对象,它继承自构造器函数的原型对象
var that = Object.create(this.prototype);
//调用构造器函数,绑定 this 到新对象上
var other = this.apply(that,arguments);
//如果它返回值不是一个对象,就返回该新对象
return (typeof other === 'object' && other) || that;
});
其中,Object.create 方法为之前定义的。
我们可以定义一个构造器并扩充它的原型:
var Mammal = function (name) {
this.name = name;
};
Mammal.prototype.get_name = function () {
return this.name;
};
Mammal.prototype.says = function () {
return this.saying || '';
};
现在,我们可以构造一个实例:
var myMammal = new Mammal('Herb the Mammal');
var name = myMammal.get_name(); //-->Herb the Mammal
我们可以构造另一个伪类来继承 Mammal
,这是通过定义它的 constructor
函数并替换它的 prototype
为一个 Mammal
的实例来实现的:
var Cat = function (name) {
this.name = name;
this.saying = 'meow';
};
//替换 Cat.prototype 为一个新的 Mammal 实例
Cat.prototype = new Mammal();
//扩充原型对象,新增 purr 和 get_name 方法
Cat.prototype.purr = function (n) {
var i,s = '';
for(i = 0; i < n; i++){
if(s){
s += '-';
}
s += 'r';
}
return s;
};
Cat.prototype.get_name = function () {
return this.says() + ' ' + this.name + ' ' + this.says();
};
var myCat = new Cat('Henrietta');
var says = myCat.says(); //--> meow
var purr = myCat.purr(5); //--> r-r-r-r-r
var name = myCat.get_name(); //--> meow Henrietta meow
伪类模式本意是想向面向对象靠拢,但它看起来格格不入。我们可以隐藏一些丑陋的细节,通过使用 method
方法来定义一个 inherits
方法实现:
Function.method('inherits',function (Parent) {
this.prototype = new Parent();
return this;
});
我们的 inherits
方法和 method
方法都返回 this
,这样允许我们可以采用级联的形式编程。现在,我们可以只用一行语句构造我们的 Cat
对象。
var Cat = function (name) {
this.name = name;
this.saying = 'meow';
}
.inherits(Mammal)
.method('purr',function (n) {
var i,s = '';
for(i = 0; i < n; i++){
if(s){
s += '-';
}
s += 'r';
}
return s;
})
.method('get_name',function () {
return this.says() + ' ' + this.name + ' ' + this.says();
});
通过隐藏那些无谓的 prototype
细节,现在它看起来没那么怪异了。但它存在几个糟糕的问题:没有私有环境,所有属性都是公开的。无法访问 super
(父类)的方法。并且,使用构造器函数存在一个严重的危害:如果你在调用构造器函数时忘了在前面加上前缀 new
,那么 this
将不会被绑定到一个新对象上,而是被绑定到全局对象上,这样一来,你不但没扩充新对象,反而破坏了全局变量环境。发生那样的情况时,既没有编译时警告,也没有运行时警告。
为了降低这个风险,所有构造器函数都约定命名成首字母大写的形式,且不以首字母大写的形式命名其他的东西。
在基于类的语言中,类的继承是代码重用的唯一方式,而 JavaScript 有更多且更好的选择。
对象说明符(Object Specifiers)
有时候,构造器需要接受一大堆参数。在这种情况下,如果我们在编写构造器时让它接受一个简单的对象说明符,可能会更加友好些。那个对象包含了将要构造的对象规格说明。
所以,与其这么写:
var myObject = maker(f,l,mlc,s);
不如这么写:
var myObject = maker({
first:f,
middle:m,
last:l,
state:s,
city:c
});
现在多个参数可以按任意顺序排序,如果构造器会聪明地使用默认值,一些参数可以忽略掉,并且代码也更容易阅读。
当与 JSON
一起工作时,这种形式还有一个间接的好处:JSON
文本只能描述数据,但有时候数据表示的是一个对象,把该数据与它的方法关联起来是有用的。如果构造器取得一个对象说明符,就能让它轻松实现,因为我们可以简单地把 JSON
对象传递给构造器,而它将返回一个构造完全的对象。
原型(Prototype)
在一个纯粹的原型模式中,我们会摒弃类,转而专注于对象。基于原型的继承相比于基于类的继承在概念上更为简单:一个新对象可以继承一个旧对象的属性。
让我们先用对象字面量去构造一个有用的函数:
var myMammal = {
name : 'Herb the Mammal',
get_name : function () {
return this.name;
},
says : function () {
return this.saying || '';
}
};
一旦有了一个想要的对象,我们就可以利用 之前定义的 Object.create 方法构造出更多实例来:
var myCat = Object.create(myMammal);
myCat.name = 'Henrietta';
myCat.saying = 'meow';
myCat.purr = function (n) {
var i,s = '';
for(i = 0; i < n; i++){
if(s){
s += '-';
}
s += 'r';
}
return s;
};
myCat.get_name = function () {
return this.says + ' ' + this.name + ' ' + this.says;
};
这是一种 “差异化继承” 。通过定制一个新的对象,我们指明它与所基于的基本对象的区别。
有时候,它对某些数据结构继承于其他数据结构的情形非常有用:假定我们要解析一门类似 JavaScript
或 TEX
那样用一对花括号指示作用域的语言。定义在某个作用域里定义的条目在该作用域之外是不可见的。但在某种意义上,一个内部作用域会继承它的外部作用域。JavaScript
在表示这样的关系上做得非常好。当遇到一个左花括号时 block
函数被调用。parse
函数将从 scope
中寻找符号,并且当它定义了新的符号时扩充 scope
:
var block = function () {
//记住当前作用域。构造一个包含了当前作用域中所有对象的新作用域
var oldScope = scope;
scope = Object.create(scope);
//传递左花括号作为参数调用 advance
advance('{');
//使用新的作用域进行解析
parse(scope);
//传递右花括号作为参数调用 advance 并抛弃新作用域,恢复原来老的作用域
advance('}');
scope = oldScope;
};
函数化(Functional)
迄今为止,我们所看到的继承模式的一个弱点就是没办法保护隐私。对象的所有属性都是可见的。对这种情况,我们有一个解决方案,那就是应用模块模式。
我们以小写字母开头来命名它,因为不需要使用 new
前缀。该函数包括 4 个步骤。
- 创建一个新对象。可以使用很多方法去构造一个对象:它可以构造一个字面量对象,或者可以和
new
前缀连用去调用一个构造器函数,或者它可以使用Object.create
方法去构造一个已经存在的对象的新实例,或者它可以调用任何一个会返回一个对象的函数。 - 有选择的定义私有实例变量和方法。这些就是函数中通过
var
语句定义的普通变量。 - 给这个新对象扩充方法。这些方法有权去访问参数,以及通过
var
语句定义的变量。 - 返回那个新对象。
以下是一个函数化构造器的伪代码模板:
var constructor = function(spec,my){
var that,其他的私有变量;
my = my || {};
把共享的变量和函数添加到 my 中
that = 一个新的对象;
添加给 that 的特权方法
return that;
};
其中, spec
对象包含构造器需要构造一个新实例的所有信息。spec
的内容可能会被复制到私有变量中,或是被其他函数改变,或者方法可以在需要的时候访问 spec
的信息。
my
是一个为继承链中的构造器提供秘密共享的容器。 my
对象可以选择性地使用。如果没有传入一个 my
对象,那么会创建一个 my
对象。
函数内部给 my
对象添加共享的变量和函数是通过赋值语句实现的:
my.member = value;
在我们给 that
添加特权方法时,我们可以分配一个新函数成为 that
的成员方法。或者,更安全地,我们可以先把函数定义为私有方法,然后再把它们分配给 that
:
var methodical = function(){
···
};
that.methodical = methodical;
分两步去定义 methodical
的好处是,如果其他方法想要调用 methodical
,它们可以直接调用 methodical()
而不是 that.methodical
。如果该实例被破坏或篡改,甚至 that.methodical
被替换掉了,调用 methodical
的方法同样会继续工作,因为它们私有的 methodical
不受该实例被修改的影响。
以前面的 mammal
的例子来试着使用这种模式。不过此处不需要 my
,所以我们先抛开它不管。
var mammal = function (spec) {
var that = {};
that.get_name = function () {
return spec.name;
};
that.says = function () {
return spec.saying || '';
};
return that;
};
var mammal = mammal({name:'Herb'});
在伪类模式中,构造器函数 Cat
不得不重复构造器 Mammal
已经完成的工作。在函数化模式中不再需要了,因为构造器 Cat
会调用构造器 Mammal
,让 Mammal
去做对象创建中的大部分工作,所以 Cat
只需关注自身的差异即可。
var Cat = function (spec) {
spec.saying = spec.saying || 'meow';
var that = mammal(spec);
that.purr = function (n) {
var i,s = '';
for(i = 0; i < n; i++){
if(s){
s += '-';
}
s += 'r';
}
return s;
};
that.get_name = function () {
return that.says() + ' ' + spec.name + ' ' + that.says();
};
return that;
};
var myCat = Cat({name:'Henrietta'});
函数化模式还给我们提供了一个处理父类方法的方法。我们会构造一个 superior
方法,它取得一个方法名并返回调用那个方法的函数。该函数会调用原来的方法,尽管属性已经变化了。
Object.method('superior',function (name) {
var that = this;
var method = that[name];
return function () {
return method.apply(that,arguments);
};
});
让我们在 coolcat
上试一下上面申明的函数,coolcat
相比于 cat
有一个更酷的调用父类方法的 get_name
方法。我们会申明一个 super_get_name
变量,并把调用 superior
方法所返回的结果赋值给它。
var coolcat = function (spec) {
var that = cat(spec);
var super_get_name = that.superior('get_name');
that.get_name = function (n) {
return 'like ' + super_get_name() + ' baby';
};
return that;
};
var myCoolCat = coolcat({name:'Bix'});
var name = myCoolCat.get_name(); //-->like meow Bix meow baby
函数化模式有很大的灵活性。它相比伪类模式不仅带来的工作更少,还让我们得到更好的封装和信息隐藏,以及访问父类的能力。
如果对象的所有状态都是私有的,那么该对象称为一个 “防伪” 对象。该对象的属性可以被替换或删除,但该对象的完整性不会受到损害。如果我们用函数化的样式去创建一个对象,并且该对象的所有方法都不使用 this
或 that
,那么该对象就是持久的。一个持久性对象就是一个简单功能的函数集合。
一个持久性的对象不会被入侵。访问一个持久性的对象时,除非有方法授权,否则攻击者不能访问对象的内部状态。
部件(Parts)
我们可以从一套部件中把对象组装出来。例如,我们可以构造一个给任何对象添加简单事件处理特性的函数。它会给对象添加一个 on
方法、一个 fire
方法和一个私有的事件注册表对象:
var eventuality = function (that) {
var registry = {};
that.fire = function (event) {
/*
在一个对象上触发一个事件。该事件可以是一个包含事件名称的字符串,或是一个
拥有包含事件名称的 type 属性的对象。
通过 on 方法注册的事件处理程序中匹配事件名称的函数将被调用
*/
var array,func,handler,i,
type = typeof event === 'string' ? event : event.type;
//如果这个事件存在一组事件处理程序,那么就遍历它们并按顺序依次执行
if(registry.hasOwnProperty(type)){
array = registry[type];
for(i = 0; i < array.length; i++){
handler = array[i];
//每个处理程序包含一个方法和一组可选的参数
//如果该方法是一个字符串形式的名字,那么寻找到该函数
func = handler.method;
if(typeof func === 'string'){
func = this[func];
}
//调用一个处理程序。如果该条目包含参数,那么传递它们过去。否则,传递该事件对象。
func.apply(this,
handler.parameter || [event]);
}
}
return this;
};
that.on = function (type,method,parameters) {
//注册一个事件。构造一条处理程序条目。将它插入到处理程序数组中
//如果这种类型的事件还不存在,就构造一个
var handler = {
method : method,
parameters : parameters
};
if(registry.hasOwnProperty(type)){
registry[type].push(handler);
}else{
registry[type] = [handler];
}
return this;
};
return that;
};
我们可以在任何单独的对象上调用 eventuality
,授予它事件处理方法。我们也可以赶在 that
被返回前在一个构造器函数中调用它。
eventuality(that);
用这种方式,一个构造器函数可以从一套部件中把对象组装出来。如果我们想要 eventuality
访问该对象的私有状态,可以把私有成员集 my
传给它。
参考书籍:《JavaScript 语言精粹》 - - - Douglas Crockford 著、赵泽欣等译