《JavaScript权威指南第7版》第9章 类

本文详细介绍了JavaScript中的类,从基于原型的继承到ES6的Class关键字。首先,讨论了类和原型的关系,指出类是一组从相同原型对象继承的对象,构造函数用于初始化对象状态。接着,解释了类与构造函数的联系,包括构造函数的instanceof行为、constructor属性及其在原型链中的作用。然后,深入探讨了ES6的Class语法,包括静态方法、getter/setter、字段定义以及子类化和super关键字的使用。最后,通过示例展示了如何创建复数类和集合类的层次结构,以及如何使用组合代替继承来实现功能。
摘要由CSDN通过智能技术生成

第6章介绍了JavaScript对象。那一章把每个对象都看作是一组独特的属性集合,与任何其它对象都不同。但是,定义一类共享某些属性的对象通常是有用的。类的成员或实例有自己的属性来保存或定义其状态,但它们也有定义其行为的方法。这些方法由类定义,并由所有实例共享。例如,假设有一个名为Complex的类,它表示并执行对复数的算术运算。一个复杂实例将具有一些属性来保存复数的实部和虚部(状态)。复杂类将定义方法来执行这些数字的加法和乘法(行为)。

在JavaScript中,类使用基于原型的继承:如果两个对象从同一个原型继承属性(通常是函数值属性或方法),那么我们就说这些对象是同一个类的实例。简单地说,这就是JavaScript类的工作方式。在§6.2.3和§6.3.2中介绍了JavaScript原型和继承,要理解本章,您需要熟悉这些章节中的内容。本章涵盖了§9.1中的原型。

如果两个对象继承了相同的原型,这通常(但不一定)意味着它们是由相同的构造函数或工厂函数创建和初始化的。构造函数已经在§4.6、§6.2.2和§8.2.3中介绍过,本章在§9.2中有更多内容。

JavaScript一直允许定义类。ES6引入了全新的语法(包括class关键字),使创建类更加容易。这些新的JavaScript类的工作方式与旧式类相同,本章首先解释创建类的旧方法,因为这更清楚地展示了如何在幕后使类工作。一旦我们解释了这些基本原理,我们将改变并开始使用新的、简化的类定义语法。

如果您熟悉Java或c++等强类型的面向对象编程语言,您会注意到JavaScript类与这些语言中的类有很大的不同。它们在语法上有一些相似之处,而且您可以在JavaScript中模拟“经典”类的许多特性,但是最好预先了解JavaScript的类和基于原型的继承机制与Java和类似语言的类和基于类的继承机制有本质上的不同。

9.1 类和原型

在JavaScript中,类是一组从相同原型对象继承属性的对象。因此,原型对象是类的核心特性。第6章介绍了Object. create()函数,它返回一个新创建的对象,该对象继承自一个指定的原型对象。如果我们定义了一个原型对象,然后使用Object.create()创建从它继承的对象,那么我们就定义了一个JavaScript类。通常,类的实例需要进一步初始化,通常需要定义一个创建和初始化新对象的函数。示例9-1演示了这一点:它为表示一系列值的类定义了原型对象,还定义了创建和初始化类的新实例的工厂函数。

例子 9-1. 一个简单的JavaScript类

// 这是一个工厂函数,它返回一个新的range对象。
function range(from, to) {
   
    // 使用Object .create()创建一个继承下面定义的原型对象的对象。
    // 原型对象作为这个函数的属性存储,并为所有range对象定义共享方法(行为)。
    let r = Object.create(range.methods);
    // 存储这个新range对象的起始点和结束点(状态)。
	// 这些是该对象唯一的非继承属性。
    r.from = from;
    r.to = to;
    // 最后返回新对象
    return r;
}
// 这个原型对象定义了所有range对象继承的方法。
range.methods = {
   
    //如果x在范围内,返回true,否则返回false
	//此方法适用于文本和日期范围以及数字范围。
    includes(x) {
    return this.from <= x && x <= this.to; },
    //使类的实例可迭代的生成器函数。
	//注意,它只对数值范围有效。
    *[Symbol.iterator]() {
   
        for (let x = Math.ceil(this.from); x <= this.to; x++) yield x;
    },
    // 返回range的字符串表示形式
    toString() {
    return "(" + this.from + "..." + this.to + ")"; }
};
// 下面是使用range对象的示例。
let r = range(1, 3); // 创建一个range对象
r.includes(2) // => true: 2 在范围中
r.toString() // => "(1...3)"
[...r] // => [1, 2, 3]; 通过迭代器转换为数组

在示例9-1的代码中有几点值得注意:

  • 这段代码定义了工厂函数range(),用于创建新的range对象。
  • 它使用range()函数的methods属性作为存储定义类的原型对象的方便位置。将原型对象放在这里并没有什么特别或惯用的地方。
  • range()函数定义了每个range对象上的from和to属性。这些是未共享的、非继承的属性,它们定义每个单独范围对象的唯一状态。
  • range.methods对象使用ES6简化语法来定义方法,这就是为什么在任何地方都看不到function关键字的原因。(参见§6.10.5来回顾对象字面量方法简写语法。)
  • 原型中的一个方法具有计算名称(§6.10.2)Symbol.iterator,这意味着它为范围对象定义了一个迭代器。这个方法的名称以*作为前缀,这表明它是一个生成器函数而不是常规函数。迭代器和生成器将在第12章中详细介绍。现在,这个Range类的实例可以与For /of循环和…展开运算符一起使用。
  • range.methods内定义的共享、继承方法都使用range()工厂函数中初始化的from和to属性。为了引用它们,它们使用this关键字来引用调用它们的对象。这种用法是任何类方法的基本特征。

9.2 类和构造函数

示例9-1演示了一种定义JavaScript类的简单方法。但是,这不是惯用的方法,因为它没有定义构造函数。构造函数是为初始化新创建的对象而设计的函数。使用new关键字调用构造函数,如§8.2.3所述。构造函数调用使用new自动创建新对象,因此构造函数本身只需要初始化新对象的状态。构造函数调用的关键特性是,构造函数的prototype属性被用作新对象的原型。§6.2.3介绍了原型,强调了几乎所有对象都有原型,只有少数对象有原型属性。最后,我们要澄清一点:函数对象具有prototype属性。这意味着用相同的构造函数创建的所有对象都继承相同的对象,因此它们是相同类的成员。例9-2展示了如何改变例9-1的Range类,使其使用构造函数而不是工厂函数。例9-2演示了在不支持ES6 class 关键字的JavaScript版本中创建类的惯用方法。尽管class关键字现在都可以支持了,仍有大量的旧的JavaScript代码定义的类没有使用class关键字,你应该熟悉这些惯用写法,这样您就可以阅读旧代码, 而且当你使用class关键字的时候也能够明白“底层原理”。

示例 9-2. 使用构造函数的Range类

// 这是一个初始化新的范围对象的构造函数。
// 注意,它不创建或返回对象。它只是初始化this。
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; },

    //使类的实例可迭代的生成器函数。
	//注意,它只对数值范围有效。
    [Symbol.iterator]: function*() {
   
        for(let x = Math.ceil(this.from); x <= this.to; x++) yield x;
    },

    //返回范围的字符串表示形式
    toString: function() {
    return "(" + this.from + "..." + this.to + ")"; }
};

//下面是这个新的Range类的示例用法
let r = new Range(1,3); // 创建一个Range对象;注意new的用法
r.includes(2) // => true: 2在范围内
r.toString() // => "(1...3)"
[...r] // => [1, 2, 3]; 通过迭代器转换为数组

值得相当仔细地比较示例9-1和9-2,并注意这两种定义类的技术之间的差异。首先,请注意,在将range()工厂函数转换为构造函数时,我们将其重命名为Range()。这是一种非常常见的编码约定:在某种意义上,构造函数定义了类,而类的名称(按照约定)以大写字母开头。常规函数和方法的名称以小写字母开头。

接下来,请注意使用new关键字调用Range()构造函数(在示例的最后),而不使用new关键字调用Range()工厂函数。例9-1使用常规函数调用(§8.2.1)来创建新对象,例9-2使用构造函数调用(§8.2.3)。因为Range()构造函数是用new调用的,所以它不必调用Object. create()或采取任何操作来创建新对象。在调用构造函数之前自动创建新对象,并且可以将其作为this值访问。Range()构造函数只需对其进行初始化。构造函数甚至不需要返回新创建的对象。构造函数调用自动创建一个新对象,将构造函数作为该对象的方法调用,并返回新对象。构造函数调用与常规函数调用如此不同,这是我们为构造函数命名以大写字母开头的另一个原因。构造函数被编写为使用new关键字作为构造函数调用,如果作为常规函数调用,它们通常无法正常工作。将构造函数与常规函数区分开来的命名约定有助于程序员知道何时使用new。

构造函数和new.target

在函数体中,可以通过特殊表达式new.target判断函数是否被作为构造函数调用。如果定义了该表达式的值,那么您就知道该函数是作为一个构造函数调用的,带有new关键字。当我们在§9.5中讨论子类的时候,我们会看到new.target并不总是引用它所使用的构造函数:它也可能引用子类的构造函数。
如果new.target值是undefined,则包含函数是作为函数调用的,没有使用new关键字。JavaScript的各种错误的构造函数的调用都是因为没有使用new,如果你想在构造函数中模仿这个功能,你可以这样写:

function C() {
    
	if (!new.target) return new C();
	// 初始化代码在这里
}

这种技术只适用于以这种老式方式定义的构造函数。使用class关键字创建的类不允许在没有new的情况下调用其构造函数。

示例9-1和9-2之间的另一个关键区别是原型对象的命名方式。在第一个示例中,原型是range.methods。这是一个方便和描述性的名称,但是比较随意。在第二个例子中,原型是Range.prototype,这个名称是强制规定的。调用Range()构造函数会自动使用Range.prototype作为新Range对象的原型。

最后,还要注意示例9-1和9-2之间没有变化的事情:两个类都以相同的方式定义和调用range方法。因为示例9-2演示了在ES6之前的JavaScript版本中创建类的惯用方法,所以它没有在prototype对象中使用ES6方法简写语法,而是使用function关键字显式地说明方法。但是您可以看到,两个示例中方法的实现是相同的。

重要的是,请注意,这两个range示例在定义构造函数或方法时都没有使用箭头函数。回想一下§8.1.3中定义的箭头函数没有prototype属性,因此不能用作构造函数。此外,箭头函数从定义它们的上下文继承this关键字,而不是根据调用它们的对象来设置它,这使得它们对于方法毫无用处,因为方法的定义特征是它们使用this来引用它们被调用的实例。

幸运的是,新的ES6类语法不允许使用箭头函数定义方法,因此我们就不会不小心犯这种错误。我们将很快讨论ES6 class关键字,但是首先,有更多关于构造函数的细节。

9.2.1 构造函数,类的标识和instanceof

正如我们所看到的,原型对象是类标识的基础:两个对象是同一个类的实例,当且仅当它们从同一个原型对象继承时。初始化新对象状态的构造函数不是基本的:两个构造函数可能具有指向同一原型对象的原型属性。然后,两个构造函数都可以用来创建同一个类的实例。

尽管构造函数不像原型那么基本,但构造函数充当了类的公共形象。最明显的是,构造函数的名称通常被用作类的名称。例如,我们说Range()构造函数创建了Range对象。然而,更基本的是,当测试对象是否属于某个类时,构造函数被用作instanceof操作符的右操作数。如果我们有一个对象r,想知道它是否是一个Range对象,我们可以这样写:

r instanceof Range // => true: r 继承自 Range.prototype

instanceof操作符的描述见§4.9.4。左边的操作数应该是正在测试的对象,右边的操作数应该是命名类的构造函数。如果o继承了C.prototype,那么表达式o instanceof C的值为真。继承不需要是直接继承:如果o继承了一个对象,而这个对象又继承了一个从C.prototype继承的对象,那么表达式的值仍然为真。

从技术上讲,在前面的代码示例中,instanceof操作符没有检查r是否真的被Range构造函数初始化了。相反,它检查r是否继承了Range.prototype。如果我们定义一个函数Strange()并将其原型设置为与Range.prototype相同,那么用new Strange()创建的对象就instanceof而言将被视为Range对象(它们实际上不会作为Range对象工作,因为它们的from和to属性还没有初始化):

function Strange() {
   }
Strange.prototype = Range.prototype;
new Strange() instanceof Range // => true

即使instanceof不能实际验证构造函数的使用,它仍然使用构造函数作为它的右操作数,因为构造函数是类的公共标识。

如果您想要测试特定原型的对象的原型链,并且不想使用构造函数作为中介,那么可以使用isPrototypeOf()方法。例如,在例9-1中,我们定义了一个没有构造函数的类,因此无法对该类使用instanceof。相反,我们可以用下面的代码来测试对象r是否是无构造函数类的成员:

range.methods.isPrototypeOf(r); // range.methods 是原型对象.

9.2.2 constructor属性

在例9-2中,我们设置了Range.prototype指向了一个包含类方法的新对象。虽然将这些方法表示为单个对象对象字面量的属性很方便,但实际上并不需要创建一个新对象。任何常规的JavaScript函数(不包括箭头函数、生成器函数和异步函数)都可以用作构造函数1,而构造函数调用需要一个prototype属性。因此,每个常规JavaScript函数自动拥有一个prototype属性。这个属性是一个对象,包含了一个具有不可枚举的constructor属性。constructor属性的值是函数对象本身:

let F = function() {
   }; // 一个函数对象.
let p = F.prototype; // 这是与F相关的原型对象.
let c = p.constructor; // 这是与原型相关联的函数.
c === F // => true: 对于任何F,F.prototype.constructor === F

这个预定义的原型对象及其constructor属性的存在意味着对象通常继承引用其构造函数的constructor属性。由于构造函数作为类的公共标识,这个constructor属性指出了一个对象引用的类:

let o = new F(); // 创建一个类F的对象o
o.constructor === F // => true: constructor属性指向类

图9-1说明了构造函数、它的原型对象、从原型到构造函数的反向引用以及用构造函数创建的实例之间的关系。

构造函数、它的原型和实例
图 9-1 构造函数、它的原型和实例

注意,图9-1使用了Range()构造函数作为示例。然而,事实上,例9-2中定义的Range类会覆盖预定义的Range.prototype,使用自己的原型对象。它定义的新原型对象没有constructor属性。因此,Range类的实例如定义的那样没有constructor属性。我们可以通过显式地向原型添加一个constructor来解决这个问题:

Range.prototype = {
   
	constructor: Range, // 显式设置构造函数的反向引用
	
	/* 方法定义在这里 */
};

在旧的JavaScript代码中,你可能会看到的另一个常见技术是使用预定义的原型对象及其构造函数属性,并添加方法,每次一个,代码如下:

// 扩展预定义的Range.prototype对象,这样我们就不会重写
// 自动创建的Range.prototype.constructor属性。
Range.prototype.includes = function (x) {
   
    return this.from <= x && x <= this.to;
};
Range.prototype.toString = function () {
   
    return "(" + this.from + "..." + this.to + ")";
};

9.3 class关键字创建类

从JavaScript语言的第一个版本开始,类就一直是JavaScript的一部分,但是在ES6中,通过引入class关键字,它们最终获得了自己的语法。例9-3显示了使用这种新语法编写Range类时的情况。

例9-3. 使用class重写的Range类

class Range {
   
    constructor(from, to) {
   
        //存储此新范围对象的起点和终点(状态)。
		//这些是此对象特有的非继承属性。
        this.from = from;
        this.to = to;
    }
    //如果x在范围内,则返回true,否则返回false
	//此方法适用于文本和日期范围以及数字范围。
    includes(x) {
    return this.from <= x && x <= this.to; }
    //使类
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值