类和构造函数
上篇教程展示了在 JavaScript 中定义类的一种方法,但是不常用,因为没有定义构造函数,构造函数是用来初始化新创建的对象的。前面已经提到,需要使用 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 + ")";
}
};
我们将构造函数命名为 Range
,这里遵循了一个常见的编程约定:定义构造函数即是定义类,类名首字母要大写。并以此与普通函数进行区分。在调用构造函数之前就已经创建了新对象,所以可以通过 this
关键字获取这个新对象并对其进行初始化。构造函数也不必返回新创建的对象,其背后的执行逻辑是:构造函数先自动创建对象,然后将构造函数作为这个新对象的方法调用一次,最后返回这个新对象。
下面对上述代码进行调用,得到的结果和之前工厂函数实现的一样:
唯一的不同就是调用方式的区别:构造函数只能通过 new
关键字进行调用。
构造函数和类的标识
原型对象是类的唯一标识,当且仅当两个对象继承自同一个原型对象时,它们才是同一个类的实例。而初始化类的状态的构造函数则不能作为类的标识,只要两个构造函数的 prototype
属性指向同一个原型对象,那么这两个构造函数创建的实例就属于同一个类。
我们在使用 instanceof
运算符来检测对象是否属于某个类时也会用到构造函数:
r instanceof Range
但实际上 instanceof
运算符并不会检查 r
是否由 Range()
构造函数初始化而来,而是检查 r
是否继承自 Range.prototype
,构造函数只是类的「外在表现」。
constructor 属性
任何 JavaScript 函数都可以用作构造函数,而调用构造函数是需要用到 prototype
属性的,因此,每个 JavaScript 函数(Function.bind()
方法返回函数除外)都自动拥有一个 prototype
属性,这个属性的值是一个对象,这个对象包含唯一一个不可枚举的属性 constructor
,constructor
属性的值是一个函数对象:
可以看到构造函数的原型中存在预先定义好的 constructor
属性,这意味着对象通常继承的 constructor
均指代它们的构造函数。由于构造函数是类的「公共标识」,因此这个 construcotr
属性为对象提供了类:
我们在上面定义 Range()
构造函数时,自己实现的 Range.prototype
原型对象并不会包含 constructor
属性,因此对应的 Range
实例也不会包含这个属性。要解决这一问题,需要显式给原型添加一个构造函数:
Range.prototype = {
constructor: Range,
// 如果 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 + ")";
}
};
或者,另一种常见的解决办法是使用预定义的原型对象,而不是自己重新定义,预定义的原型对象已经包含了 construcotr
属性,然后依次给原型对象添加方法:
// range3.js 使用构造函数来实现一个能表示值的范围的类
// 这是一个构造函数,用于初始化新创建的「范围对象」
function Range(from, to) {
// 存储新的「范围对象」的起始位置和结束位置
// 这两个属性是不可继承的,每个对象都拥有唯一属性
this.from = from;
this.to = to;
}
// 扩展预定义的 Range.prototype 原型对象,而不是重写
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 + ")";
};
下图展示了构造函数与原型对象之间的关系,包括原型对象到构造函数的反向引用以及构造函数创建的实例:
无评论