《JS权威指南学习总结--9.2 类和构造函数》
内容要点:
例9-1展示了在JS中定义类的其中一种方法。但这种方法并不常用,毕竟它没有定义构造函数,构造函数是用来初始化新创建的对象的。
使用关键字new来调用构造函数会自动创建一个新对象,因此构造函数本身只需初始化这个新对象的状态即可。
调用构造函数的一个重要特征是,构造函数的prototype属性被用做新对象的原型。这意味着通过同一个构造函数创建的所有对象都继承自一个相同的对象,因此它们都是同一类的成员。
下例9-2对例9-1中的"范围类"做了修改,使用构造函数代替工厂函数:
一.例9-2 使用构造函数来定义"范围类"
//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 = new Range(1,3); //创建一个范围对象
r.includes(2); //=>true:2 在这个范围内
r.foreach(console.log); //输出1 2 3
console.log(r.toString()); //输出(1...3)
console.log(r.constructor); //Object()
例9-1和例9-2代码比较:
首先,工厂函数range()转化为构造函数时被重命名为Range()。这里遵循了一个常见的编程约定:
从某种意义上讲,定义构造函数既是定义类,并且类名首字母要大写,而普通的函数和方法都是首字母小写。
再者,注意Range()构造函数是通过new关键字调用的,而range()工厂函数则不必使用new。
例9-1通过调用普通函数来创建新对象,例9-2则使用构造函数调用来创建新对象。
由于Range()构造函数是通过new关键字调用的,因此不必调用inherit()或其他什么逻辑来创建新对象。在调用构造函数之前就已经创建了新对象,通过this关键字可以获取这个新对象,Range()构造函数只不过时初始化this而已。构造函数甚至不必返回这个新创建的对象,构造函数会自动创建对象,然后将构造函数作为这个对象的方法来调用一次,最后返回这个对象。
事实上,构造函数的命名规则(首字母大写)和普通函数是如此不同还有另外一个原因,构造函数调用和普通函数调用时不尽相同的。构造函数就是用来"构造新对象"的,它必须通过关键字new调用,如果将构造函数用做普通函数的话,往往不会正常工作。
开发者可以通过命名约定来(构造函数首字母大写,普通函数首字母小写)判断是否应当在函数之前冠以关键字new。
例9-1和例9-2之间还有一个非常重要的区别,就是原型对象的命名。在第一段示例代码中的原型是range.methods。这种命名方式很方便同时具有很好的语义,但又过于随意。
在第二段示例代码中的原型是Range.prototype,这是一个强制的命名。对Range()构造函数的调用会自动使用Range.prototype作为Range对象的原型。
二.构造函数和类的标识
上文提到,原型对象是类的唯一标识:当且仅当两个对象继承自同一个原型对象时,它们才是属于同一个类的实例。而 初始化对象的状态 的 构造函数 则不能作为类的标识,两个构造函数的prototype属性可能指向同一个原型对象。那么这两个构造函数创建的实例是属于同一个类的。
尽管构造函数不像原型那么基础,但构造函数是类的"外在表现"。很明显的,构造函数的名字通常用做 类名。
比如,我们说Range()构造函数创建Range对象。然而,更根本地讲,当使用instanceof运算符来检测对象是否属于某个类时会用到构造函数。
假设这里有一个对象r,我们想知道r是否是Range对象,我们这样写:
r instanceof Range //如果r继承自Range.prototype,则返回true
实际上instanceof运算符并不会检查r是否由Range()构造函数初始化而来,而会检查r是否继承自Range.prototype。不过,instanceof的语法则强化了 "构造函数是类的公有标识"的概念。
三.constuctor属性
在例9-2中,将Range.prototype定义为一个新对象,这个对象包含类所需要的方法。其实没有必要新创建一个对象,用单个对象直接量的属性就可以方便地定义原型上的方法。
任何JS函数都可以用做构造函数,并且调用构造函数是需要用到一个prototype属性的。因此,每个JS函数(ES5中的Function.bind()方法返回的函数除外)都自动拥有一个prototype属性。
这个属性的值是一个对象,这个对象包含唯一一个不可枚举属性constructor。constructor属性的值是一个函数对象:
var F = function(){}; //这是一个函数对象
var p = F.prototype; //这是F相关联的原型对象
var c = p.constructor; //这是与原型相关联的函数
c === F //=>true:对于任意函数F.prototype.constructor==F
可以看到 构造函数的原型 中存在预先定义好的constructor属性,这意味着 对象 通常继承的constructor均指代它们的构造函数。由于构造函数是类的"公共标识",因此这个constructor属性为对象提供了类。
var o = new F(); //创建类F的一个对象
o.constructor ===F //=>true,constructor属性指代这个类
需要注意的是,实际上,例9-2中定义的Range类使用它自身的一个新对象重写预定义的Range.prototype对象。这个新定义的原型对象不含有constructor属性。因此Range类的实例也不含有constructor属性。我们可以通过补救措施来修正这个问题,显式给原型添加一个构造函数:
Range.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 + ")"; };
console.log(r.constructor); //Range(from,to){...}