JavaScript高级程序设计第四版学习记录-第八章对象、类与面向对象编程(二)(继承 / 类)

8.3 继承

继承是面向对象编程中讨论最多的话题。
很多面向对象语言都支持两种继承:接口继承和实现继承。
前者只继承方法签名,后者继承实际的方法。
接口继承在 ECMAScript 中是不可能的,因为函数没有签名。
实现继承是 ECMAScript 唯一支持的继承方式,而这主要是通过原型链实现的。

8.3.1 原型链

ECMA-262 把原型链定义为 ECMAScript 的主要继承方式。其基本思想就是通过原型继承多个引用类型的属性和方法。

重温一下构造函数、原型和实例的关系:每个构造函数(A)都有一个原型对象(A Prototype),原型有一个属性(constructor)指回构造函数,而实例(instance)有一个内部指针[[Prototype]]指向原型

如果原型是另一个类型的实例,则意味着这个原型本身有一个内部指针指向另一个原型,相应地另一个原型也有一个指针指向另一个构造函数。
这样就在实例和原型之间构造了一条原型链。这就是原型链的基本构想

实现原型链涉及如下代码模式:

function SuperType() { 
 this.property = true; 
} 
SuperType.prototype.getSuperValue = function() { 
 return this.property; 
}; 
function SubType() { 
 this.subproperty = false; 
} 
// 继承 SuperType 
SubType.prototype = new SuperType(); 
SubType.prototype.getSubValue = function () {
 return this.subproperty; 
}; 
let instance = new SubType(); 
console.log(instance.getSuperValue()); // true

以上代码定义了两个类型:SuperTypeSubType, 分别定义了一个属性和一个方法。
SuperType 定义了property 属性和getSuperValue() 原型方法
SubType 定义了subproperty 属性和getSubValue () 原型方法

这两个类型的主要区别是SubType 通过创建 SuperType 的实例并将其赋值给自己的原型 SubType. prototype 实现了对 SuperType 的继承
这个赋值重写了 SubType 最初的原型,将其替换为SuperType 的实例。
这意味着 SuperType 实例可以访问的所有属性和方法也会存在于 SubType.prototype

这样实现继承之后,代码紧接着又给 SubType.prototype,也就是这个 SuperType 的实例添加了一个新方法。
最后又创建了 SubType 的实例并调用了它继承的 getSuperValue()方法。

下图展示了子类的实例与两个构造函数及其对应的原型之间的关系。
在这里插入图片描述

这个例子中实现继承的关键,是 SubType 没有使用默认原型,而是将其替换成了一个新的对象。这个新的对象恰好是 SuperType 的实例。(代码 SubType.prototype = new SuperType();)

这样一来,SubType 的实例不仅能从 SuperType 的实例中继承属性和方法,而且还与 SuperType 的原型挂上了钩。于是 instance(通过内部的[[Prototype]])指向SubType.prototype,而 SubType.prototype(作为 SuperType 的实例又通过内部的[[Prototype]]指向SuperType.prototype

注意,getSuperValue()方法还在 SuperType.prototype 对象上,而 property 属性则在 SubType.prototype 上。这是因为 getSuperValue()是一个原型方法,而property 是一个实例属性。SubType.prototype 现在是 SuperType 的一个实例,因此 property才会存储在它上面。

还要注意,由于 SubType.prototypeconstructor 属性被重写为指向SuperType,所以 instance.constructor 也指向 SuperType
在这里插入图片描述

原型链扩展了原型搜索机制。
在读取实例上的属性时,首先会在实例上搜索这个属性。如果没找到,则会继承搜索实例的原型。在通过原型链实现继承之后,搜索就可以继承向上,搜索原型的原型。

对前面的例子而言,调用 instance.getSuperValue()经过了 3 步搜索:instanceSubType.prototypeSuperType.prototype,最后一步才找到这个方法。对属性和方法的搜索会一直持续到原型链的末端。

1. 默认原型 Object

实际上,原型链中还有一环。默认情况下,所有引用类型都继承自 Object,这也是通过原型链实现的。任何函数的默认原型都是一个 Object 的实例,这意味着这个实例有一个内部指针指向Object.prototype
这也是为什么自定义类型能够继承包括 toString()valueOf()在内的所有默认方法的原因。

因此前面的例子还有额外一层继承关系。下图展示了完整的原型链。

SubType 继承 SuperType,而 SuperType 继承 Object。在调用 instance.toString()时,实际上调用的是保存在 Object.prototype 上的方法。
在这里插入图片描述

2. 原型与继承关系 instanceof , isPrototypeOf()

原型与实例的关系可以通过两种方式来确定。

(1) 第一种方式是使用 instanceof 操作符。
如果一个实例的原型链中出现过相应的构造函数,则 instanceof 返回 true

console.log(instance instanceof Object); // true 
console.log(instance instanceof SuperType); // true 
console.log(instance instanceof SubType); // true 

从技术上讲,instanceObjectSuperTypeSubType 的实例,因为 instance 的原型链中包含这些构造函数的原型。结果就是 instanceof对所有这些构造函数都返回 true

(2) 第二种方式是使用 isPrototypeOf()方法。原型链中的每个原型都可以调用这个方法,只要原型链中包含这个原型,这个方法就返回 true

console.log(Object.prototype.isPrototypeOf(instance)); // true 
console.log(SuperType.prototype.isPrototypeOf(instance)); // true 
console.log(SubType.prototype.isPrototypeOf(instance)); // true

例: 判断是否是数组
在这里插入图片描述

3. 关于方法 (覆盖/增加, 对象字面量方式重写原型链)

(1) 子类有时候需要覆盖父类的方法,或者增加父类没有的方法。为此,这些方法必须在原型赋值之后再添加到原型上

function SuperType() { 
 this.property = true; 
} 
SuperType.prototype.getSuperValue = function() { 
 return this.property; 
}; 
function SubType() { 
 this.subproperty = false; 
} 
// 继承 SuperType 原型赋值
SubType.prototype = new SuperType(); 

// 定义SubType的新方法
SubType.prototype.getSubValue = function () { 
 return this.subproperty; 
}; 
// 覆盖已有的方法 原型链上已经存在getSuperValue但在这里被遮蔽
SubType.prototype.getSuperValue = function () { 
 return false; 
}; 

// SubType的实例 调用的方法是覆盖后的新getSuperValue 返回false
let instance = new SubType(); 
console.log(instance.getSuperValue()); // false 

// SuperType的实例 调用的方法是SuperType.prototype上原有的getSuperValue返回true
let instance2 = new SuperType(); 
console.log(instance2.getSuperValue()); // true

第一个方法 getSubValue()SubType 的新方法,第二个方法 getSuperValue()是原型链上已经存在但在这里被遮蔽的方法。后面在 SubType 实例上调用 getSuperValue()时调用的是这个方法。而 SuperType 的实例仍然会调用最初的getSuperValue()方法。
重点在于上述两个方法都是在把原型赋值为 SuperType 的实例之后定义的。

(2) 以对象字面量方式创建原型方法会破坏之前的原型链,因为这相当于重写了原型链

function SuperType() { 
 this.property = true; 
} 
SuperType.prototype.getSuperValue = function() { 
 return this.property; 
}; 
function SubType() { 
 this.subproperty = false; 
}
// 继承 SuperType 
SubType.prototype = new SuperType(); 

// 通过对象字面量添加新方法,这会导致上一行无效
SubType.prototype = { 
 getSubValue() { 
 return this.subproperty; 
 }, 
 someOtherMethod() { 
 return false; 
 } 
}; 
let instance = new SubType(); 
console.log(instance.getSuperValue()); // 出错!

在这段代码中,子类的原型在被赋值为 SuperType 的实例后,又被一个对象字面量覆盖了。覆盖后的原型是一个 Object 的实例,而不再是 SuperType 的实例。因此之前的原型链就断了。SubTypeSuperType 之间也没有关系了。

4. 原型链的问题

原型链虽然是实现继承的强大工具,但它也有问题。
(1) 主要问题出现在原型中包含引用值的时候。前面在谈到原型的问题时也提到过,原型中包含的引用值会在所有实例间共享,因此属性通常会在构造函数中定义而不会定义在原型上在使用原型实现继承时,原型实际上变成了另一个类型的实例。这意味着原先的实例属性变成了原型属性

下面的例子揭示了这个问题:

function SuperType() { 
 this.colors = ["red", "blue", "green"]; 
} 
function SubType() {} 
// 继承 SuperType 
SubType.prototype = new SuperType(); 
let instance1 = new SubType(); 
instance1.colors.push("black"); 
console.log(instance1.colors); // "red,blue,green,black" 
let instance2 = new SubType(); 
console.log(instance2.colors); // "red,blue,green,black" 

在这个例子中,SuperType 构造函数定义了一个 colors 属性,其中包含一个数组(引用值)。每个 SuperType 的实例都会有自己的 colors 属性,包含自己的数组。
但是,当 SubType 通过原型继承SuperType 后,SubType.prototype 变成了 SuperType 的一个实例,因而也获得了自己的 colors属性。这类似于创建了 SubType.prototype.colors 属性。最终结果是,SubType 的所有实例都会共享这个 colors 属性, 因此 instance1.colors 上的修改也能反映到 instance2.colors上。

在这里插入图片描述

(2) 原型链的第二个问题是,子类型在实例化时不能给父类型的构造函数传参。事实上,我们无法在不影响所有对象实例的情况下把参数传进父类的构造函数。再加上之前提到的原型中包含引用值的问题,就导致原型链基本不会被单独使用

5. 原型链补充总结

https://github.com/mqyqingfeng/Blog/issues/2

(1) 原型:每一个JavaScript对象(null除外)在创建的时候就会与之关联另一个对象,这个对象就是原型,每一个对象都会从原型"继承"属性。

(2) prototype
函数的 prototype 属性指向了一个对象,即调用该构造函数而创建的实例的原型。

(3) _proto_ 是每一个JavaScript对象(除了 null )都具有的一个属性,指向该对象的原型。
(即前面书中采用的[[prototype]], 可以在浏览器控制台查看)

(4) constructor,每个原型都有一个 constructor 属性指向关联的构造函数

(5) 当读取实例的属性时,如果找不到,就会查找与对象关联的原型中的属性,如果还查不到,就去找原型的原型,一直找到最顶层为止。

(6) 默认情况下,所有引用类型都继承自 Object,任何函数的默认原型都是一个 Object 的实例,这意味着这个实例有一个内部指针指向Object.prototype

Object.prototype.__proto__ 的值为 null , 即Object.prototype 没有原型。null 表示“没有对象”,即该处不应该有值。
所以查找属性的时候查到 Object.prototype 就可以停止查找了
在这里插入图片描述
在这里插入图片描述

8.3.2 盗用构造函数(对象伪装/经典继承) - call(thisArg, arg1, arg2, …) / apply(thisArg, [argsArray])方法

为了解决原型包含引用值导致的继承问题,一种叫作“盗用构造函数”(constructor stealing)的技术在开发社区流行起来(这种技术有时也称作“对象伪装”或“经典继承”)。

基本思路很简单:在子类构造函数中调用父类构造函数。因为毕竟函数就是在特定上下文中执行代码的简单对象,所以可以使用apply()和 call()方法以新创建的对象为上下文来执行构造函数

在这里插入图片描述

function SuperType() { 
 this.colors = ["red", "blue", "green"]; 
} 
function SubType() { 
 // 继承 SuperType 
 SuperType.call(this); 
} 
let instance1 = new SubType(); 
instance1.colors.push("black"); 
console.log(instance1.colors); // "red,blue,green,black" 
let instance2 = new SubType(); 
console.log(instance2.colors); // "red,blue,green" 

在这里插入图片描述
代码SuperType.call(this); 展示了盗用构造函数的调用。
通过使用 call()(或 apply())方法,SuperType构造函数在为 SubType 的实例创建的新对象的上下文中执行了。这相当于新的 SubType 对象上运行了SuperType()函数中的所有初始化代码。结果就是每个实例都会有自己的 colors 属性

(只有SuperType构造函数, instance1实例, SubType.prototype原型这一层原型关系, 没有涉及SuperType的原型链)

1. 传递参数

相比于使用原型链,盗用构造函数的一个优点就是可以在子类构造函数中向父类构造函数传参。

function SuperType(name){ 
 this.name = name; 
} 
function SubType() { 
 // 继承 SuperType 并传参
 SuperType.call(this, "Nicholas"); 
 
 // 实例属性
 // 在调用父类构造函数之后再给子类实例添加额外的属性
 this.age = 29; 
} 
let instance = new SubType(); 
console.log(instance.name); // "Nicholas"; 
console.log(instance.age); // 29 

在这个例子中,SuperType 构造函数接收一个参数 name,然后将它赋值给一个属性。在 SubType构造函数中调用 SuperType 构造函数时传入这个参数,实际上会在 SubType 的实例上定义 name 属性。
为确保 SuperType 构造函数不会覆盖 SubType 定义的属性,可以在调用父类构造函数之后再给子类实例添加额外的属性

2. 盗用构造函数的问题

盗用构造函数的主要缺点,也是使用构造函数模式自定义类型的问题:(在子类构造函数中调用父类构造函数) 必须在构造函数中定义方法,因此函数不能重用
此外,子类也不能访问父类原型上定义的方法,因此所有类型只能使用构造函数模式

由于存在这些问题,盗用构造函数基本上也不能单独使用。

8.3.3 组合继承(伪经典继承)

组合继承(有时候也叫伪经典继承)综合了原型链和盗用构造函数,将两者的优点集中了起来。

基本的思路是使用原型链继承原型上的属性和方法,而通过盗用构造函数继承实例属性。这样既可以把方法定义在原型上以实现重用,又可以让每个实例都有自己的属性

function SuperType(name){ 
 this.name = name; 
 this.colors = ["red", "blue", "green"]; 
} 
SuperType.prototype.sayName = function() { 
 console.log(this.name); 
}; 
function SubType(name, age){ 
 // 通过盗用构造函数来继承实例属性 传参name
 // 继承SuperType的name和colors属性
 SuperType.call(this, name); 

 // 定义实例属性
 // 在调用父类构造函数之后再给子类实例添加额外的属性
 this.age = age; 
} 
// 通过原型链继承原型上的属性和方法
// SubType的原型, 是SuperType的实例
SubType.prototype = new SuperType(); 
SubType.prototype.sayAge = function() { 
 console.log(this.age); 
}; 

let instance1 = new SubType("Nicholas", 29); 
instance1.colors.push("black"); 
console.log(instance1.colors); // "red,blue,green,black" 
instance1.sayName(); // "Nicholas"; 
instance1.sayAge(); // 29 

let instance2 = new SubType("Greg", 27); 
console.log(instance2.colors); // "red,blue,green" 
instance2.sayName(); // "Greg"; 
instance2.sayAge(); // 27 

在这里插入图片描述
在这个例子中,SuperType 构造函数定义了两个属性,namecolors,它的原型上也定义了一个方法叫 sayName()
SubType 构造函数调用了 SuperType 构造函数,传入了 name 参数,然后又定义了自己的属性 age
此外,SubType.prototype 也被赋值为 SuperType 的实例。原型赋值之后,又在这个原型上添加了新方法 sayAge()
这样,就可以创建两个 SubType 实例,让这两个实例都有自己的属性,包括 colors,同时还共享相同的方法。

组合继承弥补了原型链和盗用构造函数的不足,是 JavaScript 中使用最多的继承模式。而且组合继承也保留了 instanceof 操作符和 isPrototypeOf()方法识别合成对象的能力。

8.3.4 原型式继承 Object.create()

2006 年,Douglas Crockford 写了一篇文章:《JavaScript 中的原型式继承》(“Prototypal Inheritance in JavaScript”)。这篇文章介绍了一种不涉及严格意义上构造函数的继承方法,即使不自定义类型也可以通过原型实现对象之间的信息共享。文章最终给出了一个函数:

 // 创建一个以o为原型的临时构造函数F并返回构造函数F的实例
function object(o) { 
 function F() {}; // 创建一个临时构造函数F
 F.prototype = o; // 将临时构造函数F的原型设置为o
 return new F(); // 返回一个构造函数F的实例
} 

这个 object()函数会创建一个临时构造函数,将传入的对象赋值给这个构造函数的原型,然后返回这个临时类型的一个实例。本质上,object()是对传入的对象执行了一次浅复制

let person = { 
 name: "Nicholas", 
 friends: ["Shelby", "Court", "Van"] 
}; 

// 创建一个以person为原型的临时构造函数
// 并返回该临时构造函数的实例anotherPerson, yetAnotherPerson
let anotherPerson = object(person); // 即person对象的一次浅复制对象
anotherPerson.name = "Greg"; 
anotherPerson.friends.push("Rob"); 

let yetAnotherPerson = object(person); 
yetAnotherPerson.name = "Linda"; 
yetAnotherPerson.friends.push("Barbie"); 

console.log(person.friends); // "Shelby,Court,Van,Rob,Barbie"

在这里插入图片描述
Crockford 推荐的原型式继承适用于这种情况:
已经有一个对象,想在它的基础上再创建一个新对象。需要把这个对象先传给 object(),然后再对返回的对象进行适当修改。

在这个例子中,person 对象定义了另一个对象也应该共享的信息,把它传给 object()之后会返回一个新对象。这个新对象的原型是 person,意味着它的原型上既有原始值属性又有引用值属性。这也意味着 person.friends 不仅是person 的属性,也会跟 anotherPersonyetAnotherPerson 共享。这里实际上克隆了两个 person

ECMAScript 5 通过增加 Object.create()方法将原型式继承的概念规范化了。Object.create(proto,[propertiesObject])这个方法接收两个参数:作为新对象原型的对象,以及给新对象定义额外属性的对象(第二个可选)
在这里插入图片描述
在只有一个参数时,Object.create()与这里的 object()方法效果相同

let person = { 
 name: "Nicholas", 
 friends: ["Shelby", "Court", "Van"] 
}; 

let anotherPerson = Object.create(person); 
anotherPerson.name = "Greg"; 
anotherPerson.friends.push("Rob"); 

let yetAnotherPerson = Object.create(person); 
yetAnotherPerson.name = "Linda"; 
yetAnotherPerson.friends.push("Barbie"); 
console.log(person.friends); // "Shelby,Court,Van,Rob,Barbie"

Object.create()的第二个参数与 Object.defineProperties()的第二个参数一样:每个新增属性都通过各自的描述符来描述。以这种方式添加的属性会遮蔽原型对象上的同名属性

let person = { 
 name: "Nicholas", 
 friends: ["Shelby", "Court", "Van"] 
}; 
let anotherPerson = Object.create(person, { 
 name: { 
 value: "Greg" 
 } 
}); 
console.log(anotherPerson.name); // "Greg" 

原型式继承非常适合不需要单独创建构造函数,但仍然需要在对象间共享信息的场合。但要记住,属性中包含的引用值始终会在相关对象间共享,跟使用原型模式是一样的。

8.3.5 寄生式继承

与原型式继承比较接近的一种继承方式是寄生式继承(parasitic inheritance),也是 Crockford 首倡的一种模式。
寄生式继承背后的思路类似于寄生构造函数和工厂模式:创建一个实现继承的函数,以某种方式增强对象,然后返回这个对象
基本的寄生继承模式如下:

// 实现继承的函数object()
// 创建一个以o为原型的临时构造函数F并返回构造函数F的实例
function object(o) { 
 function F() {}; // 创建一个临时构造函数F
 F.prototype = o; // 将临时构造函数F的原型设置为o
 return new F(); // 返回一个构造函数F的实例
} 

// 寄生继承方法createAnother()
function createAnother(original){ 
 let clone = object(original); // 通过调用object函数创建一个以original为原型的构造函数的实例
 clone.sayHi = function() { // 以某种方式增强这个对象 (给这个实例添加sayHi方法)
 	console.log("hi"); 
 }; 
 return clone; // 返回这个对象
} 

在这段代码中,createAnother()函数接收一个参数,就是新对象的基准对象。这个对象 original会被传给 object()函数,然后将返回的以original为原型的新对象赋值给 clone。接着给 clone 对象添加一个新方法sayHi()。最后返回这个对象。可以像下面这样使用 createAnother()函数:

let person = { 
 name: "Nicholas", 
 friends: ["Shelby", "Court", "Van"] 
}; 
let anotherPerson = createAnother(person); 
anotherPerson.sayHi(); // "hi" 

这个例子基于 person 对象返回了一个新对象。新返回的 anotherPerson 对象具有 person 的所有属性和方法,还有一个新方法叫 sayHi()
在这里插入图片描述

寄生式继承同样适合主要关注对象,而不在乎类型和构造函数的场景。object()函数不是寄生式继承所必需的,任何返回新对象的函数都可以在这里使用。

注意 通过寄生式继承给对象添加函数会导致函数难以重用,与构造函数模式类似。

8.3.6 寄生式组合继承

组合继承其实也存在效率问题。最主要的效率问题就是父类构造函数始终会被调用两次:一次在是创建子类原型时调用,另一次是在子类构造函数中调用。
本质上,子类原型最终是要包含超类对象的所有实例属性,子类构造函数只要在执行时重写自己的原型就行了

回顾前面组合继承的例子:

// 组合继承

function SuperType(name) { 
 this.name = name; 
 this.colors = ["red", "blue", "green"]; 
} 
SuperType.prototype.sayName = function() { 
 console.log(this.name); 
}; 
function SubType(name, age){ 
 SuperType.call(this, name); // 第二次调用 SuperType() 
 this.age = age; 
} 
SubType.prototype = new SuperType(); // 第一次调用
SubType.prototype.constructor = SubType; 
SubType.prototype.sayAge = function() { 
 console.log(this.age); 
}; 

组合继承代码中两次调用 SuperType构造函数。在上面的代码执行后,SubType.prototype上会有两个属性:namecolors。它们都是SuperType的实例属性,但通过SubType.prototype = new SuperType();成为了 SubType 的原型属性。
在调用 SubType 构造函数时,也会调用 SuperType构造函数,会在新对象上创建实例属性 namecolors,遮蔽原型上同名的属性。
如图所示,有两组 namecolors 属性:一组在实例上,另一组在 SubType 的原型上。这是调用两次 SuperType构造函数的结果。好在有办法解决这个问题。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

寄生式组合继承通过盗用构造函数继承属性,但使用混合式原型链继承方法。
基本思路是不通过调用父类构造函数给子类原型赋值,而是取得父类原型的一个副本 (这就是寄生式组合继承与组合继承的区别)。
就是使用寄生式继承来继承父类原型,然后将返回的新对象赋值给子类原型

寄生式继承在这里插入图片描述

寄生式组合继承的基本模式如下所示:
// 使用寄生式继承来继承父类原型,然后将返回的新对象赋值给子类原型

// prototype 是以父类原型SuperType.prototype为原型的构造函数的实例对象, 是子类SubType的原型.

function inheritPrototype(subType, superType) { 

 // 使用寄生式继承来继承父类原型 创建父类原型的一个副本
 let prototype = object(superType.prototype); // 创建对象 
 
 prototype.constructor = subType; // 增强对象 

 // 将返回的新对象赋值给子类原型 
 subType.prototype = prototype; // 赋值对象
 
 // prototype 是以父类SuperType为原型的构造函数的实例对象, 是子类SubType的原型.
} 

这个 inheritPrototype()函数实现了寄生式组合继承的核心逻辑。
这个函数接收两个参数:子类构造函数和父类构造函数。在这个函数内部,第一步是创建父类原型的一个副本。然后,给返回的prototype 对象设置 constructor 属性,解决由于重写原型导致默认 constructor 丢失的问题。最后将新创建的对象赋值给子类型的原型。
如下例所示,调用 inheritPrototype()就可以实现前面例子中的子类型原型赋值:

function SuperType(name) { 
 this.name = name; 
 this.colors = ["red", "blue", "green"]; 
} 

SuperType.prototype.sayName = function() { 
 console.log(this.name); 
}; 

function SubType(name, age) { 
 SuperType.call(this, name);
  this.age = age; 
} 

// 实现子类型原型赋值:
inheritPrototype(SubType, SuperType); 

SubType.prototype.sayAge = function() { 
 console.log(this.age); 
}; 

在这里插入图片描述

这里只调用了一次 SuperType 构造函数,避免了 SubType.prototype 上不必要也用不到的属性,因此可以说这个例子的效率更高。而且,原型链仍然保持不变,因此instanceof 操作符和isPrototypeOf()方法正常有效。寄生式组合继承可以算是引用类型继承的最佳模式。

8.4 类

前几节深入讲解了如何只使用 ECMAScript 5 的特性来模拟类似于类(class-like)的行为。
各种策略都有自己的问题,也有相应的妥协。正因为如此,实现继承的代码也显得非常冗长和混乱。

为解决这些问题,ECMAScript 6 新引入的 class 关键字具有正式定义类的能力。
类(class)是ECMAScript 中新的基础性语法糖结构,因此刚开始接触时可能会不太习惯。虽然 ECMAScript 6 类表面上看起来可以支持正式的面向对象编程,但实际上它背后使用的仍然是原型和构造函数的概念。

8.4.1 类定义

(1) 与函数类型相似,定义类也有两种主要方式:类声明和类表达式。这两种方式都使用 class 关键字加大括号:

// 类声明
class Person {} 
// 类表达式
const Animal = class {}; 

(2) 与函数表达式类似,类表达式在它们被求值前也不能引用。
不过,与函数定义不同的是,虽然函数声明可以提升,但类定义不能:

// 函数表达式在被求值前不可引用
console.log(FunctionExpression); // undefined 
var FunctionExpression = function() {}; 
console.log(FunctionExpression); // function() {} 

// 函数声明可以提升
console.log(FunctionDeclaration); // FunctionDeclaration() {} 
function FunctionDeclaration() {} 
console.log(FunctionDeclaration); // FunctionDeclaration() {} 

// 类表达式在被求值前不可引用
console.log(ClassExpression); // undefined 
var ClassExpression = class {}; 
console.log(ClassExpression); // class {} 

// 类声明不能提升
console.log(ClassDeclaration); // ReferenceError: ClassDeclaration is not defined 
class ClassDeclaration {} 
console.log(ClassDeclaration); // class ClassDeclaration {} 

另一个跟函数声明不同的地方是,函数受函数作用域限制,而类受块作用域限制

{ 
 function FunctionDeclaration() {} 
 class ClassDeclaration {} 
} 
console.log(FunctionDeclaration); // FunctionDeclaration() {} 
console.log(ClassDeclaration); // ReferenceError: ClassDeclaration is not defined

(3) 类的构成
类可以包含构造函数方法、实例方法、获取函数、设置函数和静态类方法,但这些都不是必需的。
空的类定义照样有效。默认情况下,类定义中的代码都在严格模式下执行。
与函数构造函数一样,多数编程风格都建议类名的首字母要大写,以区别于通过它创建的实例(比如,通过 class Foo {}创建实例 foo

// 空类定义,有效 
class Foo {} 
// 有构造函数的类,有效
class Bar { 
 constructor() {} 
} 
// 有获取函数的类,有效
class Baz { 
 get myBaz() {} 
} 
// 有静态方法的类,有效
class Qux { 
 static myQux() {} 
} 

类表达式的名称是可选的。在把类表达式赋值给变量后,可以通过 name 属性取得类表达式的名称字符串。但不能在类表达式作用域外部访问这个标识符。

let Person = class PersonName { 
 identify() { 
 console.log(Person.name, PersonName.name); 
 } 
} 
let p = new Person(); 
p.identify(); // PersonName PersonName 
console.log(Person.name); // PersonName 
console.log(PersonName); // ReferenceError: PersonName is not defined

8.4.2 类构造函数 constructor

constructor 关键字用于在类定义块内部创建类的构造函数。
方法名 constructor 会告诉解释器在使用 new 操作符创建类的新实例时,应该调用这个函数。
构造函数的定义不是必需的,不定义构造函数相当于将构造函数定义为空函数。

1. 实例化

使用 new 操作符实例化 Person 的操作等于使用 new 调用其构造函数。唯一可感知的不同之处就是,JavaScript 解释器知道使用 new和类意味着应该使用 constructor 函数进行实例化。

使用 new 调用类的构造函数会执行如下操作。
(1) 在内存中创建一个新对象。
(2) 这个新对象内部的[[Prototype]]指针被赋值为构造函数的 prototype 属性。
(3) 构造函数内部的 this 被赋值为这个新对象(即 this 指向新对象)。
(4) 执行构造函数内部的代码(给新对象添加属性)。
(5) 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象。

class Animal {} 
class Person { 
 constructor() { 
 	console.log('person ctor'); 
 } 
} 
class Vegetable { 
 constructor() { 
 	this.color = 'orange'; 
 } 
} 
let a = new Animal(); 
let p = new Person(); // person ctor 
let v = new Vegetable(); 
console.log(v.color); // orange

类实例化时传入的参数会用作构造函数的参数。如果不需要参数,则类名后面的括号也是可选的:

class Person { 
 constructor(name) { 
 	console.log(arguments.length); 
 	this.name = name || null; 
 } 
} 
let p1 = new Person; // 0 
console.log(p1.name); // null 
let p2 = new Person(); // 0 
console.log(p2.name); // null 
let p3 = new Person('Jake'); // 1 
console.log(p3.name); // Jake 

默认情况下,类构造函数会在执行之后返回 this 对象。

构造函数返回的对象会被用作实例化的对象,如果没有什么引用新创建的 this 对象,那么这个对象会被销毁。不过,如果返回的不是 this 对象,而是其他对象,那么这个对象不会通过 instanceof 操作符检测出跟类有关联,因为这个对象的原型指针并没有被修改。

class Person { 
 constructor(override) { 
	 this.foo = 'foo'; 
	 if (override) { 
		 return { 
		 	bar: 'bar' 
		 }; 
	 } 
 } 
} 
let p1 = new Person(), 
 p2 = new Person(true); 
console.log(p1); // Person{ foo: 'foo' }  返回的是this对象
console.log(p1 instanceof Person); // true 
console.log(p2); // { bar: 'bar' } 
console.log(p2 instanceof Person); // false 

类构造函数与构造函数的主要区别是,调用类构造函数必须使用 new 操作符。而普通构造函数如果不使用 new 调用,那么就会以全局的 this(通常是 window)作为内部对象。调用类构造函数时如果忘了使用 new 则会抛出错误

构造函数也是函数(使用new操作符-构造函数, 不使用new操作符-普通函数,this指向Global对象,在浏览器中是window对象)>
在这里插入图片描述

function Person() {} 
class Animal {} 
// 把 window 作为 this 来构建实例
let p = Person(); 
let a = Animal(); 
// TypeError: class constructor Animal cannot be invoked without 'new' 

类构造函数没有什么特殊之处,实例化之后,它会成为普通的实例方法
(但作为类构造函数,仍然要使用 new 调用), 因此,实例化之后可以在实例上引用它

严格来说, 类中定义的普通方法都定义在原型上, 所以constructor并不是实例本身的方法, 而是实例从原型继承的方法, 只有在构造函数中定义在this上的方法才是实例自己的方法。
在这里插入图片描述

class Person {} 
// 使用类创建一个新实例
let p1 = new Person(); 

p1.constructor(); 
// TypeError: Class constructor Person cannot be invoked without 'new' 

// 使用对类构造函数的引用创建一个新实例
let p2 = new p1.constructor(); 

2. 把类当成特殊函数

ECMAScript 中没有正式的类这个类型。ECMAScript 类就是一种特殊函数。声明一个类之后,通过 typeof 操作符检测类标识符,表明它是一个函数

class Person {} 
console.log(Person); // class Person {} 
console.log(typeof Person); // function

类标识符有 prototype 属性,而这个原型也有一个 constructor 属性指向类自身

class Person{} 
console.log(Person.prototype); // { constructor: f() } 
console.log(Person === Person.prototype.constructor); // true 

与普通构造函数一样,可以使用 instanceof 操作符检查构造函数原型是否存在于实例的原型链中

class Person {} 
let p = new Person(); 
console.log(p instanceof Person); // true 

由此可知,可以使用 instanceof 操作符检查一个对象与类构造函数,以确定这个对象是不是类的实例。只不过此时的类构造函数要使用类标识符,比如,在前面的例子中要检查 pPerson

如前所述,类本身具有与普通构造函数一样的行为。在类的上下文中,类本身在使用 new 调用时就会被当成构造函数

// 以下内容在图灵视频的讲解中. 讲者勘误说有问题
重点在于,类中定义的constructor 方法不会被当成构造函数,在对它使用instanceof 操作符时会返回 false。但是,如果在创建实例时直接将类构造函数当成普通构造函数来使用,那么 instanceof 操作符的返回值会反转

class Person {} 

// 使用Person类定义p1, p1被视为Person的实例 
let p1 = new Person(); 
console.log(p1.constructor === Person); // true 
console.log(p1 instanceof Person); // true 
console.log(p1 instanceof Person.constructor); // false 
Person.constructor === Function // true
// Person类是由Function构造函数创建的, 因此任何一个类的constructor属性都等于 Function, 因此, p1创建的是对象实例而不是函数实例, 所以p1 instanceof Person.constructor)为 false


// 使用Person.constructor这个类构造函数来定义p1, 相当于new Function, p1为Person.constructor的函数实例, 而非Person的实例
let p2 = new Person.constructor(); 
console.log(p2.constructor === Person); // false 
console.log(p2 instanceof Person); // false 
console.log(p2 instanceof Person.constructor); // true 
console.log(p2.constructor === Person.constructor); // true
// p1为Person.constructor的函数实例, 即Function实例, 任何一个类的constructor属性都等于Function
p2 instanceof Function // true
p2 instanceof Person.constructor // true
Person.constructor === Function // true

类是 JavaScript 的一等公民,可以在任何地方定义,可以像其他对象或函数引用一样把类作为参数传递

// 类可以像函数一样在任何地方定义,比如在数组中
let classList = [ 
 class { // 作为数组的第零项
	 constructor(id) { 
		 this.id_ = id; 
		 console.log(`instance ${this.id_}`); 
	 } 
 } 
]; 
function createInstance(classDefinition, id) { 
	return new classDefinition(id); 
} 

// 作为函数参数传递
let foo = createInstance(classList[0], 3141); // instance 3141 

与立即调用函数表达式相似,类也可以立即实例化:

// 因为是一个类表达式,所以类名是可选的
let p = new class Foo { 
 constructor(x) { 
 	console.log(x); 
 } 
}('bar'); // bar 
console.log(p); // Foo {}

// 等同于以下代码, 先声明后实例化
class Foo { 
 constructor(x) { 
 	console.log(x); 
 } 
}
let p = new Foo('bar'); // bar 

8.4.3 实例、原型和类成员

类的语法可以非常方便地定义应该存在于实例上的成员应该存在于原型上的成员,以及应该存在于类本身的成员

1. 实例成员 - 在类构造函数constructor中定义, 用于类实例

每次通过new调用类标识符时,都会执行类构造函数constructor。在这个函数内部,可以为新创建的实例(this)添加“自有”属性。至于添加什么样的属性,则没有限制。另外,在构造函数执行完毕后,仍然可以给实例继续添加新成员。

每个实例都对应一个唯一的成员对象,这意味着所有成员都不会在原型上共享
添加到 this 的所有内容都会存在于不同的实例上

class Person { 
 constructor() { 
	 // 这个例子先使用对象包装类型定义一个字符串
	 // 为的是在下面测试两个对象的相等性
	 this.name = new String('Jack'); 
	 this.sayName = () => console.log(this.name); 
	 this.nicknames = ['Jake', 'J-Dog']; 
 } 
} 

let p1 = new Person(), p2 = new Person(); 
p1.sayName(); // Jack 
p2.sayName(); // Jack 

// 每个实例的所有成员都不共享
console.log(p1.name === p2.name); // false 
console.log(p1.sayName === p2.sayName); // false 
console.log(p1.nicknames === p2.nicknames); // false 

p1.name = p1.nicknames[0]; 
p2.name = p2.nicknames[1]; 
p1.sayName(); // Jake 
p2.sayName(); // J-Dog

2. 原型方法与访问器 - 在类块中定义,用于类的原型对象

在类的上下文中,类本身在使用 new 调用时就会被当成构造函数

为了在实例间共享方法,类定义语法把在类块{}中定义的方法作为原型方法,在类块中定义的所有内容都会定义在类的原型上。

// 有两个同名的locate方法, 分别是实例方法和原型方法
class Person { 
 constructor() { 
	// 添加到 this 的所有内容都会存在于不同的实例上
	this.locate = () => console.log('instance'); 
 }
  // 在类块中定义的所有内容都会定义在类的原型上
 locate() { 
 	console.log('prototype'); 
 } 
} 
let p = new Person(); // 类的实例P 
p.locate(); // instance  实例方法
Person.prototype.locate(); // prototype 原型方法 
p.__proto__.locate(); // prototype 调用原型方法
console.log(p.__proto__ === Person.prototype) // true 实例的_proto_即为Person构造函数的原型

可以把方法定义在类构造函数constructor() {}中或者类块{}中,但不能在类块中给原型添加原始值或对象作为成员数据

class Person { 
 name: 'Jake' 
} 
// Uncaught SyntaxError: Unexpected token 

类方法等同于对象属性,因此可以使用字符串、符号或计算的值作为键

const symbolKey = Symbol('symbolKey'); 
class Person { 
 stringKey() { // 字符串
 	console.log('invoked stringKey'); 
 } 
 [symbolKey]() { // 符号
 	console.log('invoked symbolKey'); 
 } 
 ['computed' + 'Key']() { // 计算值 中括号语法设置对象属性
 	console.log('invoked computedKey'); 
 } 
} 
let p = new Person(); 
p.stringKey(); // invoked stringKey 
p[symbolKey](); // invoked symbolKey 中括号语法读取对象属性
p.computedKey(); // invoked computedKey

类定义也支持获取和设置访问器。语法与行为跟普通对象一样

class Person { 
 set name(newName) { 
 	this.name_ = newName; 
 } 
 get name() { 
 	return this.name_; 
 } 
} 
let p = new Person(); 
p.name = 'Jake'; 
console.log(p.name); // Jake

在这里插入图片描述
在这里插入图片描述

3. 静态类方法 在类定义中使用static关键字作为前缀,用于类本身

可以在类上定义静态方法。这些方法通常用于执行不特定于实例的操作,也不要求存在类的实例。
与原型成员类似,静态成员每个类上只能有一个。
静态类成员在类定义中使用 static 关键字作为前缀。在静态成员中,this 引用类自身。其他所有约定跟原型成员一样:

class Person { 
 constructor() { 
	 // 添加到 this 的所有内容都会存在于不同的实例上
	 this.locate = () => console.log('instance', this); 
 } 
 // 定义在类的原型对象上
 locate() { 
 	console.log('prototype', this); 
 } 
 // 定义在类本身上
 static locate() { 
 	console.log('class', this); 
 } 
} 
let p = new Person(); 
// 类实例上的locate方法
p.locate(); // instance, Person {} 
// 类原型对象上的locate方法
Person.prototype.locate(); // prototype, {constructor: ... } 
// 类本身上的locate方法
Person.locate(); // class, class Person {} 

在这里插入图片描述
静态类方法非常适合作为实例工厂:

class Person { 
 constructor(age) { 
 	this.age_ = age; 
 } 
 sayAge() { 
 	console.log(this.age_); 
 } 
 static create() { 
	// 使用随机年龄创建并返回一个 Person 实例
	return new Person(Math.floor(Math.random()*100)); 
 } 
} 
console.log(Person.create()); // Person { age_: ... }

4. 非函数原型和类成员

虽然类定义并不显式支持在原型或类上添加成员数据,但在类定义外部,可以手动添加

不能给原型添加原始值或者对象作为成员数据
在这里插入图片描述

class Person { 
 sayName() { 
 	console.log(`${Person.greeting} ${this.name}`); 
 } 
} 
// 在类上定义数据成员
Person.greeting = 'My name is';
// 在原型上定义数据成员
Person.prototype.name = 'Jake'; 
let p = new Person(); 
p.sayName(); // My name is Jake

注意 类定义中之所以没有显式支持添加数据成员,是因为在共享目标(原型和类)上添加可变(可修改)数据成员是一种反模式。一般来说,对象实例应该独自拥有通过 this引用的数据。

5. 迭代器与生成器方法

定义生成器
使用星号增强 yield 的行为,让它能够迭代一个可迭代对象,从而一次产出一个值。
yield*实际上只是将一个可迭代对象序列化为一连串可以单独产出的值,所以这跟把 yield放到一个循环里没什么不同
yield*的值是关联迭代器返回 done: true 时的 value 属性。
对于普通迭代器来说,这个值是undefined
在这里插入图片描述

类定义语法支持在原型和类本身上定义生成器方法:

class Person { 
 // 在原型上定义生成器方法
 *createNicknameIterator() { 
	 yield 'Jack'; 
	 yield 'Jake'; 
	 yield 'J-Dog'; 
 } 
 // 在类上定义生成器方法
 static *createJobIterator() { 
	 yield 'Butcher'; 
	 yield 'Baker'; 
	 yield 'Candlestick maker'; 
 } 
} 
let jobIter = Person.createJobIterator(); 
console.log(jobIter.next().value); // Butcher 
console.log(jobIter.next().value); // Baker 
console.log(jobIter.next().value); // Candlestick maker 
let p = new Person(); 
let nicknameIter = p.createNicknameIterator(); 
console.log(nicknameIter.next().value); // Jack 
console.log(nicknameIter.next().value); // Jake 
console.log(nicknameIter.next().value); // J-Dog 

因为支持生成器方法,所以可以通过添加一个默认的迭代器,把类实例变成可迭代对象

class Person { 
 constructor() { 
 	this.nicknames = ['Jack', 'Jake', 'J-Dog']; 
 } 
 *[Symbol.iterator]() { // 默认迭代器
 	yield *this.nicknames.entries(); 
 } 
} 
let p = new Person(); 
for (let [idx, nickname] of p) { 
 console.log(nickname); 
} 
// Jack 
// Jake 
// J-Dog 

也可以只返回迭代器实例

class Person { 
 constructor() { 
 	this.nicknames = ['Jack', 'Jake', 'J-Dog']; 
 } 
 [Symbol.iterator]() { // 返回迭代器实例
 	return this.nicknames.entries(); 
 } 
} 
let p = new Person(); 
for (let [idx, nickname] of p) { 
 console.log(nickname); 
} 
// Jack 
// Jake 
// J-Dog

8.4.4 继承

前面8.3章节花了大量篇幅讨论如何使用 ES5 的机制实现继承。
ECMAScript 6 新增特性中最出色的一个就是原生支持了类继承机制。
虽然类继承使用的是新语法,但背后依旧使用的是原型链。

1. 继承基础 extends关键字

ES6 类支持单继承。使用 extends 关键字,就可以继承任何拥有[[Construct]]和原型的对象。
很大程度上,这意味着不仅可以继承一个类,也可以继承普通的构造函数(保持向后兼容)

class Vehicle {} 
// 继承类
class Bus extends Vehicle {} 

let b = new Bus(); 
console.log(b instanceof Bus); // true 
console.log(b instanceof Vehicle); // true 


function Person() {} 
// 继承普通构造函数
class Engineer extends Person {} 

let e = new Engineer(); 
console.log(e instanceof Engineer); // true 
console.log(e instanceof Person); // true 

派生类都会通过原型链访问到类和原型上定义的方法。this 的值会反映调用相应方法的实例或者类

class Vehicle { 
 identifyPrototype(id) { 
 	console.log(id, this); 
 }
 static identifyClass(id) { 
 	console.log(id, this); 
 } 
} 
class Bus extends Vehicle {} 
let v = new Vehicle(); 
let b = new Bus(); 
b.identifyPrototype('bus'); // bus, Bus {} 
v.identifyPrototype('vehicle'); // vehicle, Vehicle {} 
Bus.identifyClass('bus'); //bus, class Bus extends Vehicle {}
Vehicle.identifyClass('vehicle'); // vehicle, class Vehicle {}

在这里插入图片描述
注意 extends 关键字也可以在类表达式中使用,因此 let Bar = class extends Foo {}是有效的语法。

2. 构造函数、HomeObject 和 super()

派生类的方法可以通过 super 关键字引用它们的原型。这个关键字只能在派生类中使用,而且仅限于类构造函数、实例方法和静态方法内部。

在实例方法中, super引用父类原型, 可以通过它调用父类原型上的方法
在类方法(静态方法)中,super引用父类,可以调用父类的静态方法
在构造函数中,super是父类构造函数的别名, 通过它可以调用父类构造函数

(1) 在类构造函数中使用 super 可以调用父类构造函数

class Vehicle { 
 constructor() { 
 	this.hasEngine = true; 
 } 
} 
class Bus extends Vehicle { 
 constructor() { 
	 // 不要在调用 super()之前引用 this,否则会抛出 ReferenceError 
	 super(); // 相当于 super.constructor() 
	 console.log(this instanceof Vehicle); // true 
	 console.log(this); // Bus { hasEngine: true } 
 } 
} 
new Bus(); 

在这里插入图片描述

(2) 在静态方法中可以通过 super 调用继承的类上定义的静态方法

class Vehicle { 
 static identify() { 
 	console.log('vehicle'); 
 } 
} 
class Bus extends Vehicle { 
 static identify() { 
 	super.identify(); 
 } 
} 
Bus.identify(); // vehicle

注意, ES6 给类构造函数constructor和静态方法static添加了内部特性[[HomeObject]],这个特性是一个指针,指向定义该方法的对象。这个指针是自动赋值的,而且只能在 JavaScript 引擎内部访问。super 始终会定义为[[HomeObject]]的原型。

(3) 在使用 super 时要注意几个问题。

super 只能在派生类的构造函数constructor和静态方法static中使用。

// 报错
class Vehicle { 
 constructor() { 
 super(); 
 // SyntaxError: 'super' keyword unexpected 
 } 
} 

 不能单独引用 super 关键字,要么用它调用构造函数,要么用它引用静态方法。

class Vehicle {} 

class Bus extends Vehicle { 
 constructor() { 
 	console.log(super); 
 	// SyntaxError: 'super' keyword unexpected here 
 } 
} 

 直接调用 super()会调用父类构造函数,并将返回的实例赋值给 this。

class Vehicle {} 

class Bus extends Vehicle { 
 constructor() { 
	 super(); 
	 console.log(this instanceof Vehicle); 
 } 
} 
new Bus(); // true 

super()的行为如同调用构造函数,如果需要给父类构造函数传参,则需要手动传入。

class Vehicle { 
 constructor(licensePlate) { 
 	this.licensePlate = licensePlate; 
 } 
} 

class Bus extends Vehicle { 
 constructor(licensePlate) { 
 	super(licensePlate);  // 手动传参licensePlate
 } 
} 

console.log(new Bus('1337H4X')); // Bus { licensePlate: '1337H4X' } 

 如果派生类没有定义类构造函数,在实例化派生类时会自动调用super()(进而调用父类构造函数,并将返回的实例赋值给 this),而且会传入所有传给派生类的参数

class Vehicle { 
 constructor(licensePlate) { 
 	this.licensePlate = licensePlate; 
 } 
} 

class Bus extends Vehicle {} 

console.log(new Bus('1337H4X')); // Bus { licensePlate: '1337H4X' } 

 在类构造函数中,不能在调用 super()之前引用 this

class Vehicle {} 

class Bus extends Vehicle { 
 constructor() { 
 	console.log(this); 
 } 
} 
new Bus(); 
// ReferenceError: Must call super constructor in derived class 
// before accessing 'this' or returning from derived constructor 

 如果在派生类中显式定义了构造函数constructor,则要么必须在其中调用 super(),要么必须在其中返回一个对象。

class Vehicle {} 

class Car extends Vehicle {} // 没有显示定义构造函数

class Bus extends Vehicle { 
 constructor() { 
 	super();  // 调用super()
 } 
} 

class Van extends Vehicle { 
 constructor() { 
 	return {};  // 返回一个对象
 } 
} 
console.log(new Car()); // Car {} 
console.log(new Bus()); // Bus {} 
console.log(new Van()); // {}

在这里插入图片描述
如果在派生类中显式定义了构造函数但没有调用super()且没有返回一个对象, 则定义时不报错但创建实例初始化对象时会报错。
未捕获的引用错误:派生类的构造函数在访问this或返回前,必须调用父类的构造函数。派生类构造函数中的this来自于对父类构造函数的调用。

在这里插入图片描述

3. 抽象基类 new.target (用于定义一个可被其他类继承但本身不会被实例化的类)

抽象基类是非完整实现的类,可用于定义抽象方法并限制派生类必须实现其定义的抽象方法。另外,也可以预定义或实现一些公共方法,供派生类继承。只定义抽象方法的抽象基类有时候也被称为特型,即只规定子类的接口名称。

有时候可能需要定义这样一个类,它可供其他类继承,但本身不会被实例化。
虽然 ECMAScript 没有专门支持这种类的语法 ,但通过 new.target 也很容易实现。

new.target 保存通过 new 关键字调用的类或函数。
通过在实例化时检测 new.target 是不是抽象基类,通过throw new Error() 可以阻止对抽象基类的实例化

// 抽象基类 
class Vehicle { 
 constructor() { 
	 console.log(new.target); 
	 if (new.target === Vehicle) { 
	 	throw new Error('Vehicle cannot be directly instantiated');
	 } 
 } 
} 
// 派生类
class Bus extends Vehicle {}
 
new Bus(); // class Bus {} 

// 实例化时检测到new.target 是Vericle这个基类而不是其他派生类, throw Error阻止对抽象基类的实例化
new Vehicle(); // class Vehicle {} 
// Error: Vehicle cannot be directly instantiated 

另外,通过在抽象基类构造函数中进行检查,可以要求派生类必须定义某个方法。
因为原型方法在调用类构造函数之前就已经存在了,所以可以通过 this 关键字来检查相应的方法

// 抽象基类
class Vehicle { 
 constructor() { 
	 if (new.target === Vehicle) { 
	 	throw new Error('Vehicle cannot be directly instantiated'); 
	 } 
	 if (!this.foo) { // 要求派生类必须定义foo方法
	 	throw new Error('Inheriting class must define foo()'); 
	 } 
	 console.log('success!'); 
 } 
} 

// 派生类没有定义类构造函数,在实例化派生类时会自动调用 `super()`,而且会传入所有传给派生类的参数

// 派生类 定义了foo方法
class Bus extends Vehicle { 
 foo() {}; // 原型方法 原型方法在调用类构造函数之前就已经存在
} 
// 派生类 未定义foo方法
class Van extends Vehicle {} 

new Bus(); // success! 
new Van(); // Error: Inheriting class must define foo() 

4. 继承内置类型

ES6 类为继承内置引用类型如Array/Set/Map等提供了顺畅的机制,开发者可以方便地扩展内置类型:

class SuperArray extends Array { 
 shuffle() { 
	 // 洗牌算法
	 for (let i = this.length - 1; i > 0; i--) { 
	 	const j = Math.floor(Math.random() * (i + 1)); 
	 	[this[i], this[j]] = [this[j], this[i]]; // 数组解构 二者互换
	 } 
 } 
} 

let a = new SuperArray(1, 2, 3, 4, 5); 

console.log(a instanceof Array); // true 
console.log(a instanceof SuperArray); // true
console.log(a); // [1, 2, 3, 4, 5] 
a.shuffle(); 
console.log(a); // [3, 1, 4, 5, 2] 

有些内置类型的方法会返回新实例。默认情况下,返回实例的类型与原始实例的类型是一致的

class SuperArray extends Array {} 

let a1 = new SuperArray(1, 2, 3, 4, 5); 
let a2 = a1.filter(x => !!(x%2)) 

console.log(a1); // [1, 2, 3, 4, 5] 
console.log(a2); // [1, 3, 5] 
console.log(a1 instanceof SuperArray); // true 
console.log(a2 instanceof SuperArray); // true 

如果想覆盖这个默认行为,则可以覆盖 Symbol.species 访问器,这个访问器决定在创建返回的实例时使用的类:

class SuperArray extends Array { 
 // 规定在创建返回的实例时使用Array
 static get [Symbol.species]() { 
 	return Array; 
 } 
}
 
let a1 = new SuperArray(1, 2, 3, 4, 5); 
// filter():对数组每一项都运行传入的函数,将函数返回 true 的项组成数组, 返回这个新数组
let a2 = a1.filter(x => !!(x%2)) 

console.log(a1); // [1, 2, 3, 4, 5] 
console.log(a2); // [1, 3, 5] 
console.log(a1 instanceof SuperArray); // true 

// 因为filter()方法返回一个新数组且SuperArray类通过Symbol.species规定了return Array, 所以a2是Array的实例
console.log(a2 instanceof SuperArray); // false 
console.log(a2 instanceof Array); // true

在这里插入图片描述

5. 类混入

把不同类的行为集中到一个类是一种常见的 JavaScript 模式。虽然 ES6 没有显式支持多类继承,但通过现有特性可以轻松地模拟这种行为。

注意 Object.assign()方法是为了混入对象行为而设计的。只有在需要混入类的行为时才有必要自己实现混入表达式。如果只是需要混入多个对象的属性,那么使用Object.assign()就可以了。
在这里插入图片描述

在下面的代码片段中,extends 关键字后面是一个 JavaScript 表达式。任何可以解析为一个类或一个构造函数的表达式都是有效的。这个表达式会在求值类定义时被求值

class Vehicle {} 
function getParentClass() { 
 console.log('evaluated expression'); 
 return Vehicle; 
} 
class Bus extends getParentClass() {} 
// 可求值的表达式

混入模式可以通过在一个表达式中连缀多个混入元素来实现,这个表达式最终会解析为一个可以被继承的类

如果 Person 类需要组合 A、B、C,则需要某种机制实现 B 继承 A,C 继承 B,而 Person再继承 C,从而把 A、B、C 组合到这个超类中。实现这种模式有不同的策略。
一个策略是定义一组“可嵌套”的函数,每个函数分别接收一个超类作为参数,而将混入类定义为这个参数的子类,并返回这个类。这些组合函数可以连缀调用,最终组合成超类表达式

class Vehicle {} 

let FooMixin = (Superclass) => class extends Superclass { 
 foo() { 
 	console.log('foo'); 
 } 
}; 

let BarMixin = (Superclass) => class extends Superclass { 
 bar() { 
 	console.log('bar'); 
 } 
}; 

let BazMixin = (Superclass) => class extends Superclass { 
 baz() { 
 	console.log('baz'); 
 } 
}; 

class Bus extends FooMixin(BarMixin(BazMixin(Vehicle))) {} 

let b = new Bus(); 
b.foo(); // foo 
b.bar(); // bar 
b.baz(); // baz 

在这里插入图片描述

通过写一个辅助函数,可以把嵌套调用展开:

class Vehicle {} 
let FooMixin = (Superclass) => class extends Superclass { 
 foo() { 
 console.log('foo'); 
 } 
}; 
let BarMixin = (Superclass) => class extends Superclass { 
 bar() { 
 console.log('bar'); 
 } 
}; 
let BazMixin = (Superclass) => class extends Superclass { 
 baz() { 
 console.log('baz'); 
 } 
}; 


// 展开嵌套调用
function mix(BaseClass, ...Mixins) { 
 return Mixins.reduce((accumulator, current) => current(accumulator), BaseClass); 
} 

class Bus extends mix(Vehicle, FooMixin, BarMixin, BazMixin) {} 
let b = new Bus(); 
b.foo(); // foo 
b.bar(); // bar 
b.baz(); // baz

在这里插入图片描述

8.5 小结

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值