继承是代码重用的模式。JavaScript 可以模拟基于类的模式,还支持其它更具表现力的模式。但保持简单通常是最好的策略。
JavaScript 是基于原型的语言,也就是说它可以直接继承其他对象。
1 伪类
JavaScript 的原型不是直接让对象从其他对象继承,而是插入一个多余的间接层:通过构造函数来产生对象。
当一个函数被创建时,Function 构造器产生的函数对象会运行这样类似的代码:
this.prototype = {constructor : this};
新的函数对象新增了一个 prototype 属性,它是一个包含了 constructor 属性且属性值为该新函数的对象。
当采用构造器调用模式,即用 new 去调用一个函数时,它会这样执行:
Function.method('new', function (){
var that = Object.create(this.prototype);//创建一个继承了构造器函数的原型对象的新对象
var other = this.apply(that, arguments);//调用构造器函数,绑定 this 到新对象
return (typeof other === 'object' && other) || that;//如果构造器函数的返回值不是对象,就直接返回这个新对象
});
我们可以定义一个构造器,然后扩充它的原型:
//定义构造器并扩充原型
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');
console.log(myMammal.get_name());//Herb the mammal
构造另一个伪类来继承 Mammal(定义构造器函数并替换它的 prototype):
var Cat = function (name) {
this.name = name;
this.saying = 'meow';
};
Cat.prototype = new Mammal();
扩充原型:
Cat.prototype.purr = function (n) {
var i, s = '';
for (i = 0; i < n; i += 1) {
if (s) {
s += '-';
}
s += 'r';
}
return s;
};
Cat.prototype.get_name = function () {
return this.says() + ' ' + this.name + ' ' + this.says();
};
var myCat = new Cat('Henrietta');
console.log(myCat.says());//meow
console.log(myCat.purr(5));//r-r-r-r-r
console.log(myCat.get_name());//meow Henrietta meow
我们使用 method 方法定义了 inherits 方法,来隐藏上面这些丑陋的细节:
/**
* 为 Function.prototype 新增 method 方法
* @param name 方法名称
* @param func 函数
* @returns {Function}
*/
Function.prototype.method = function (name, func) {
if (!this.prototype[name])//没有该方法时,才添加
this.prototype[name] = func;
return this;
};
Function.method('inherits', function (Parent) {
this.prototype = new Parent();
return this;
});
这两个方法都返回 this,这样我们就可以以级联的方式编程啦O(∩_∩)O~
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 += 1) {
if (s) {
s += '-';
}
s += 'r';
}
return s;
}).method('get_name', function () {
return this.says() + ' ' + this.name + ' ' + this.says();
});
var myCat = new Cat('Henrietta');
console.log(myCat.says());//meow
console.log(myCat.purr(5));//r-r-r-r-r
console.log(myCat.get_name());//meow Henrietta meow
虽然我们有了行为很像“类”的构造器函数,但没有私有环境,所有的属性都是公开的,而且不能访问父类的方法。
如果在调用构造函数时忘记加上 new 前缀,那么 this 就不会被绑定到新对象上,而是被绑定到了全局变量!!!这样我们不但没有扩充新对象,还破坏了全局变量环境。
这是一个严重的语言设计错误!为了降低出现这个问题的概率,所有的构造器函数都约定以首字母大写的形式来命名。这样当我们看到首字母大写的形式的函数,就知道它是构造器函数啦O(∩_∩)O~
当然,更好的策略是根本不使用构造器函数。
2 对象说明符
有时候,构造器需要接受一大堆参数,这很麻烦。所以在编写构造器时,让它接受一个简单的对象说明符会更好:
var myObject = maker({
first: f,
middle: m,
last: l
});
现在这些参数可以按照任意的顺序排列咯,而且构造器还能够聪明地为那些没有传入的参数使用默认值,代码也变得更易阅读啦O(∩_∩)O~
3 原型
基于原型的继承指的是,一个新对象可以继承一个旧对象的属性。首先构造出一个有用的对象,然后就可以构造出更多与那个对象类似的对象。
/**
* 原型
*/
var myMammal = {
name: 'Herb the mammal',
get_name: function () {
return this.name;
},
says: function () {
return this.saying || '';
}
};
//创建新实例
var myCat = Object.create(myMammal);
myCat.name = 'Henrietta';
myCat.saying = 'meow';
myCat.purr = function (n) {
var i, s = '';
for (i = 0; i < n; i += 1) {
if (s) {
s += '-';
}
s += 'r';
}
return s;
};
myCat.get_name = function () {
return this.says() + ' ' + this.name + ' ' + this.says();
};
console.log(myCat.says());//meow
console.log(myCat.purr(5));//r-r-r-r-r
console.log(myCat.get_name());//meow Henrietta meow
这里用到了 create 方法来创建新的实例:
Object.create = function (o) {
var F = function () {
};
F.prototype = o;
return new F();
}
4 函数化
目前为止看到的继承模式的问题是:无法保护隐私,对象的所有属性都是可见的。有一些无知的程序员会使用伪装私有的模式,即给一个需要私有的属性起一个古怪的名字,并希望其他使用代码的程序员假装看不到它们!
其实有更好的方法:应用模块模式。
我们先构造一个生成对象的函数,它有这些步骤:
- 创建新对象。这有四种方式:
【1】构造一个对象字面量。
【2】调用一个构造器函数。
【3】构造一个已存在对象的新实例。
【4】调用任意一个会返回对象的函数。 - 定义私有实例变量与方法。
- 为这个新对象扩充方法,这些方法拥有特权去访问这些参数。
- 返回这个新对象。
函数化构造器的伪代码如下:
var constructor = function (spec, my){
var that, 其他私有变量;
my = my || {};
把共享的变量和函数添加到 my 中
that = 一个新对象
添加给 that 的特权方法
return that;
};
spec 对象包含了需要构造一个新实例的所有信息,它可以被用到到私有变量或者其他函数中。
my 对象为在一个继承链中的构造器提供了共享的容器,如果没有传入,那么会创建一个 my 对象。
创建特权方法的方式是:把函数定义为私有方法,然后再把它们分配给 that:
var methodical = function (){
...
};
that.methodical = methodical;
这样分两步定义的好处是:私有的 methodical 不受这个实例被改变的影响。
现在,我们把这个模式应用到 mammal 示例中:
var mammal = function (spec) {
var that = {};
that.get_name = function () {
return spec.name;
};
that.says = function () {
return spec.saying || '';
};
return that;
};
var myMammal = mammal({name: 'Herb'});
console.log(myMammal.get_name());//Herb
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 += 1) {
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'});
console.log(myCat.says());//meow
console.log(myCat.purr(5));//r-r-r-r-r
console.log(myCat.get_name());//meow Henrietta meow
函数化模式还能调用父类的方法。这里我们构造一个 superior 方法,它会返回调用某个方法名的函数:
//返回调用某个方法名的函数
Object.method('superior', function (name) {
var that = this,
method = that[name];
return function () {
return method.apply(that, arguments);
};
});
现在创建一个 coolcat,它拥有一个可以调用父类方法的 get_name:
var coolcat = function (spec) {
var that = cat(spec),
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'});
console.log(myCoolCat.get_name());//like meow Bix meow baby
函数化模式有很大的灵活性,而且可以更好地实现封装、信息隐藏以及访问父类方法的能力。
如果对象所有的状态都是私有的,那么就称为防伪对象。这个对象的属性可以被替换或删除,但这个对象的状态不受影响。如果用函数化模式来创建对象,并且这个对象的所有方法都不使用 this 或 that,那么这个对象就是持久性的,它不会被入侵。除非存在特权方法,否则不能访问这个持久性对象的内部状态。
5 事件处理函数
可以构造一个能够给任何对象添加简单事件处理特性的函数。这里,我们给这个对象添加一个 on 方法,fire 方法和私有的事件注册对象:
var eventuality = function (that) {
var registry = {};
/**
* 触发事件
*
* 使用 'on' 方法注册的事件处理程序将被调用
* @param 可以是包含事件名称的字符串,或者是一个拥有 type 属性(值为事件名称)的对象。
*/
that.fire = function (event) {
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 += 1) {
handler = array[i];//处理程序包含一个方法和一组可选的参数
func = handler.method;
if (typeof func === 'string') {//如果方法是字符串形式的名称,则寻找它
func = this[func];
}
//调用它。如果处理程序包含参数,则传递过去,否则就传递事件对象
func.apply(this, handler.parameters || [event]);
}
}
return this;
};
/**
* 注册一个事件
* @param type
* @param method
* @param parameters
*/
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);
JavaScript 弱类型的特性在此是一个巨大的优势,因为我们无须处理对象继承关系中的类型O(∩_∩)O~