类和原型
在javascript中,类的所有实例对象都从同一个原型对象上继承属性。因此,原型对象是类的核心。下面例子中定义了 inherit() 函数,这个函数返回一个新创建的对象,后者继承自某个原型对象。如果定义一个原型对象,然后通过 inherit() 函数创建一个继承自它的对象,这样就定义了一个javascript类。==通常,类的实例还需要进一步的初始化,通常是通过定义一个函数创建并初始化这个新对象==。下面例子演示了给一个“值的范围”的类定义了原型对象,还定义了一个“工厂”函数用以创建并初始化的实例。
例 9-1:一个简单的javascript类
//这个工厂方法返回一个新的“range对象”
function range(from, to){
//使用inherit()函数创建对象,“这个对象继承自在下面定义的原型对象”
//“原型对象作为函数的一个属性存储”,并定义所有“范围对象”所共享的方法(行为)
var r = inherit(range.methods);
//存储新的“范围对象”的起始位置和结束位置
//这两个属性是不可继承的,每个对象都拥有唯一的属性
r.from = from;
r.to = to;
//返回这个新创建的对象
return r;
}
//原型对象定义方法,这些方法为每个range对象所继承
range.methods = {
//如果x在范围内,则返回true,否则返回false
//这个方法可以比较数字范围,也可以比较字符串和日期范围
includes: function(x){
return this.from <= x && x <= this.to;
},
//对于范围内的每个整数都调用一次f
//这个方法只可用作数字范围
foreach: function(f){
for(var x = Math.ceil(this.from); x <= this.to; x++){
f(x);
}
},
//返回表示这个范围的字符串
toString: function(){
return "(" + this.from + "..." + this.to + ")";
}
};
//这里是使用“range对象”的一些例子
var r = range(1, 3);
r.includes(2); //true
r.foreach(console.log); //输出1、2、3
console.log(r.toString()); //输出(1...3)
function inherit(p){
if(p == null)
throw TypeError(); // p 是一个对象,但不能是null(和undefined)
if(Object.create) //如果Object.create()存在
return Object.create(p); //直接使用它
var t = typeof p; //否则进行进一步检测
if(t !== "object" && t !== "function")
throw TypeError();
function f(){}; //定义一个空构造函数
f.prototype = p; //将其原型属性设置为p
return new f(); //使用f()创建p的继承对象
}
类和构造函数
例9-1展示了在javascript中定义类的其中一种方法。但这种方法并不常用,毕竟它没有定义构造函数,构造函数是用来初始化新创建的对象的。使用关键字new来调用构造函数。使用new调用构造函数会自动创建一个新对象,因此构造函数本身只需初始化这个新对象的状态即可。调用构造函数的一个重要特征是,构造函数的prototype属性被用作新对象的原型。这意味着通过同一个构造函数创建的所有对象都继承自一个相同的对象,因此他们都是同一个类的对象。例9-2对例9-1中的“范围类”做了修改,使用构造函数代替工厂函数:
以下一小段代码用以解释上面重点部分的话:
var o = {x: "aaaa"};
function f(){}
f.prototype = o;
var result = new f();
//调用构造函数的一个重要特征是,
//构造函数的prototype属性(f.prototype)被用作新对象的原型(result.__proto__)
console.log(result.__proto__ === f.prototype);
例9-2:使用构造函数来定义“范围类”
//这是一个构造函数,用以初始化新创建的“范围对象”
//注意,这里并没有创建并返回一个对象,仅仅是初始化
function Range(from, to){
//存储新的“范围对象”的起始位置和结束位置
//这两个属性是不可继承的,每个对象都拥有唯一的属性
this.from = from;
this.to = to;
}
//所有的“范围对象”都继承自这个对象
//注意,属性的名字必须是“prototype”
Range.prototype = {
//如果x在范围内,则返回true,否则返回false
//这个方法可以比较数字范围,也可以比较字符串和日期范围
includes: function(x){
return this.from <= x && x <= this.to;
},
//对于范围内的每个整数都调用一次f
//这个方法只可用作数字范围
foreach: function(f){
for(var x = Math.ceil(this.from); x <= this.to; x++){
f(x);
}
},
//返回表示这个范围的字符串
toString: function(){
return "(" + this.from + "..." + this.to + ")";
}
};
//这里是使用“range对象”的一些例子
var r = new Range(1, 3);
r.includes(2); //true
r.foreach(console.log); //输出1、2、3
console.log(r.toString()); //输出(1...3)
可以看出这两个例子代码的区别:
- 9-1例中的工厂方法range()转换成Range()(这里遵循一个常见的变成约定:从某种意义上讲,定义构造函数就是定义类,并且类名首字母要大写)
- Range()构造函数是通过new关键字来调用的,而range()工厂函数则不必使用new。
构造函数和类的标识
constructor属性
鸭式辨型
上文所描述的检测对象的类的各种技术多少都会有些问题,至少在客户端javascript中是如此。解决办法就是规避掉这些问题:不要关注“对象的类是什么”,而是关注“对象能做什么”。这种思考问题的方式在Python和Ruby中非常普遍,称为“鸭式辨型”(由作家James Whitcomb Riley提出的)。
像鸭子一样走路、游泳并且嘎嘎叫鸟就是鸭子。
用程序语言来说就是“如果一个对象可以像鸭子一样走路、游泳并且嘎嘎叫,就认为这个对象是鸭子,哪怕它并不是从鸭子类的原型对象继承而来的”。
网友评论