类和原型
在之前定义了inherit()函数,这个函数返回一个新创建的对象,后者继承自某个原型对象。如果定义一个原型对象,然后通过inherit()函数创建一个继承自它的对象,这样就定义了一个JavaScript类。
//range.js:实现一个能表示值的范围的类
//这个工厂方法返回一个新的"范围对象"
function range(from, to) {
//使用inherit()函数来创建对象,这个对象继承自在下面定义的原型对象
//原型对象作为函数的一个属性存储,并定义所有"范围对象"所共享的方法(行为)
var r = inheirt(range.methods);
//存储新的"范围对象"的起始位置和结束位置(状态)
//这两个属性的不可继承的,每个对象都拥有唯一的属性
r.from = from;
r.to = to;
//返回这个新创建的对象
return r;
}
//原型对象定义方法,这些方法为每个范围对象所继承
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 + ")";
}
}
//这里是使用"范围对象"的一些例子
var r = range(1, 3); //创建一个范围对象
r.includes(2); //=> true:2 在这个范围内
r.foreach(console.log); // 输出 1 2 3
console.log(r) //输出(1...3)
这里给range()函数定义了一个属性range.methods, 用以快捷地存放定义类的原型对象。再者,注意range()函数给每个函数对象都定义了from和to属性,用以定义范围的起实位置和结束位置,这两个属性是非共享的,当然也是不可继承的。最后,注意在range.methods中定义的那些可共享、可继承的方法都用到了from和to属性,而且使用了this关键字,为了指代它们,二者使用this关键字来指代调用这个方法的对象。
类和构造函数
使用new调用构造函数自动创建一个新对象,因此构造函数本身只需初始化这个新对象的状态即可。调用构造函数的一个重要特征是,构造函数的prototype属性被用做新对象的原型。这意味着通过同一个构造函数创建的所有对象都继承自一个相同的对象,因此它们都是同一个类的成员。下面是使用构造函数代替工厂函数:
//range2.js:表示值的范围的类的另一种实现
//这是一个构造函数,用以初始化新创建的"范围对象"
//注意,这里并没有创建并返回一个对象,仅仅是初始化
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 + ")";
}
}
var r = range(1, 3); //创建一个范围对象
r.includes(2); //=> true:2 在这个范围内
r.foreach(console.log); // 输出 1 2 3
console.log(r) //输出(1...3)
从某种意义上讲,定义构造函数即是定义类,并且类名首字母要大写。而普通的函数和方法都是首字母小写。
Range()构造函数只不过是初始化this而已。构造函数甚至不必返回这个新创建的对象,构造函数会自动创建对象,然后将构造函数作为这个对象的方法来调用一次,最后返回这个新对象。构造函数就是用来"构造新对象"的,它必须通过关键字new调用,如果将构造函数用作普通函数的话,往往不会正常工作。
构造函数和类的标识
原型对象是类的唯一标识: 当且仅当两个对象继承自同一个原型对象时,它们才是属于同一个类的实例。而初始化对象的状态的构造函数则不能作为类的标识,两个构造函数的prototype属性可能指向同一个原型对象。那么这两个构造函数创建的实例是属于同一个类的。
constructor属性
任何JavaScript函数都可以用做构造函数,并且调用构造函数是需要用到一个prototype属性。因此,每个JavaScript函数都自动拥有一个prototype属性。这个属性的值是一个对象,这个对象包含唯一一个不可枚举属性constructor。constructor属性的值是一个函数对象:
var F = function() {}; //这是一个函数对象
var P = F.prototype; //这是F相关联的原型对象
var c = P.prototype; //这是与原型相关联的函数
c === F //=>true:对于任意函数F.prototype.constructor==F
可以看到构造函数的原型中存在预先定义好的constructor属性,这意味着对象通常继承的constructor均指代它们的构造函数。由于构造函数是类的"公共标识",因此这个constructor属性为对象提供了类。
var o = new F(); //创建类F的一个对象
o.constructor === F // =>true,constructor属性指代这个类
在之前定义的Range类使用它自身的一个新对象重写预定义的Range.prototype对象。这个新定义的原型对象不含有constructor属性。因此Range类的实例也不含有constructor属性。我们可以通过补救措施来修正这个问题,显示给原型添加一个构造函数:
Rnage.prototype = {
constructor: Range, //显示设置构造函数方向引用
includes: function(x) {
return this.from <= x && x <= this.to;
},
foreach: function(f) {
for (var x = Math.ceil(this.from); x <= this.to; x++) f(x);
},
toString: function() {
return "(" + this.from + "..." + this.to + ")";
}
}
另一种是使用预定义的原型对象,预定义的原型对象包含constructor属性:
//扩展预定义的Range.prototype对象,而不重写之
//这样就自动创建Range.prototype.constructor属性
Range.prototype.includes = function(x) {
return this.from <= x && x <= this.to;
};
Range.prototype.foreach = function(f) {
for (var x = Math.ceil(this.from); x <= this.to; x++) f(x);
};
Range.prototype.toString = function() {
return "(" + this.from + "..." + this.to + ")";
}
instanceof运算符
如果o继承自c.prototype, 则表达式o instanceof c值为true。这里的继承可以不是直接继承,如果o所继承的对象继承自另一个对象,后一个对象继承自c.prototype, 这个表达式的运算结果也是true。
可以使用isPrototypeOf()方法,比如,可以通过如下代码来检测对象r是否事故定义的范围类的成员:
range.methods.isPrototypeOf(r); //range.method 是原型对象
在两个不同框架页面中创建的两个数组继承自两个相同但相互独立的原型对象,其中一个框架页面中的数组不是另一个框架页面的Array()构造函数的实例,instanceof运算结果是false。
鸭式辨型
不要关注"对象的类是什么", 而是关注"对象能做什么"。这种思考方式在Python和Ruby中非常普遍,称为"鸭式辩型"。
像鸭子一样走路、游泳并且嘎嘎叫的鸟就是鸭子。
对于JavaScript程序员来说,这句话可以理解为"如果一个对象可以像鸭子一样走路、游泳并且嘎嘎叫,就认为这个对象是鸭子,哪怕它并不是从鸭子类的原型对象继承而来的"。
在很多场景下,我们并不知道一个对象是否真的是Array的实例,当然是可以通过判断是否包含非负的length属性来得知是否Array的实例。我们说"包含一个值是非负整数的length"是数组的一个特征——“会走路”,任何具有"会走路"这个特征的对象都可以当做数组等待。然而必须要了解的是,真正数组的length属性有一些独有的行为:当添加新的元素时,数组长度会自动更新,并且当给length属性设置一个更小的整数时,数组会自动截断。我们说这些特征是"会游泳"和"嘎嘎叫"。如果所实现的代码需要"会游泳"且能"嘎嘎叫", 则不能使用只"会走路"的类似数组的对象。
鸭式辩型的实现方法让人感觉太"放任自流": 仅仅是假设输入对象实现了必要的方法,根本没有执行进一步的检查。如果输入对象没有遵循"假设",那么当代码试图调用那些不存在的方法时就会报错。另一种实现方法是对输入对象进行检查。但不是检查它们的类,而是用适当的名字来检查它们所实现的方法。这样可以将非法输入尽可能早地拦截在外,并可给出带有更多提示信息地报错。
标准转换方法
最重要的方法首当toString()。这个方法的作用是返回一个可以表示这个对象的字符串。在希望使用字符串的地方用到对象的话(比如对象用做属性名或使用"+“运算符来进行字符串连接运算),JavaScript会自动调用这个方法。如果没有实现这个方法,类会默认从Object.prototype中继承toString()方法,这个方法的运算结果是”[object Object]", 这个字符串用处不大。toString()方法应当返回一个可读的字符串,这样最终用户才能将这个输出值利用起来,然而有时候并不一定非要如此,不管怎样,可以返回可读字符串的toString()方法也会让程序调试变得更加轻松。
toLocaleString()和toString()极为类似:toLocaleString()是以本地敏感性(locale-sensitive)的方式来将对象转换为字符串。默认情况下,对象继承的toLocaleString()方法只是简单地调用toString()方法。有一些内置类型包含有用的toLocaleString()方法用以实际上返回本地化相关的字符串。如果需要为对象到字符串的转换。
第三个方法是valueOf(),它用来将对象转换为原始值。比如,当数学运算符(除了"+"运算符)和关系运算符作用于数字文本表示的对象时,会自动调用valueOf()方法。大多数对象都没有合适的原始值来表示它们,也没有定义这个方法。
第四个方法是toJSON(),这个方法是由JSON.stringify()自动调用的。JSON格式用于序列化良好的数据结构,而且可以处理JavaScript原始值、数组和纯对象。它和类无关,当对一个对象执行序列化操作时,它会忽略对象的原型和构造函数。比如将Range对象或Complex对象作为参数传入JSON.stringify(),将返回诸如{“from”:1, “to”:3}或{“r”:1,“i”:-1}这种字符串。如果将这些字符串传入JSON.parse(),则会得到一个和Range对象和Complex对象具有相同属性的纯对象,但这个对象不会包含从Range对象和Complex对象继承来的方法。
比较方法
JavaScript的相等运算符比较对象时,比较的是引用而不是值。也就是说,给定两个对象引用,如果要看它们是是否指向同一个对象,不是检查这两个对象是否具有相同的属性名和相同的属性值,而是直接比较这两个单独的对象是否相等,或者比较它们的顺序。如果定义一个类,并且希望比较类的实例,应该定义合适的方法来执行比较操作
对于简单的类,可以通过简单地比较它们的constructor属性来确保两个对象是相同类型,然后比较两个对象属性以保证它们的值相等。
如果将对象用于JavaScript的关系比较运算符,比如"<“和”<=",JavaScript会首先调用对象的valueOf()方法,如果这个方法返回一个原始值,则直接比较原始值。