ES6的继承其实可以说就是对ES5的继承机制的包装和一些优化 , 其背后还是原型链 。
继承基础
ES6 类支持单继承。使用 extends 关键字,就可以继承任何拥有[[Construct]]和原型的对象。
它会继承父类上的原型方法 , 静态成员 , 以及 constructor (需要配合 super)
很大程度上,这意味着不仅可以继承一个类,也可以继承普通的构造函数(保持向后兼容)
class Fclass1 {}
// 第一种继承方法
class Sclass1 extends Fclass1 {
// ... 子类里的内容
}
// 第二种继承方法
let Sc1 = class extends Fclass1 {
// ... 子类的内容
}
// 继承普通函数
function fun() {}
let Scfun = class extends fun {
// ... 子类的内容
}
let scl = new Sclass1()
let sc = new Sc1()
let scfun = new Scfun
console.log(scl instanceof Fclass1); // ture
console.log(sc instanceof Fclass1); // ture
console.log(scfun instanceof fun); // ture
派生类(子类)都会通过原型链访问到类和原型上定义的方法。this 的值会反映调用相应方法的实例或者类
就是 定义到原型上的方法 , 在实例调用时会返回的 this 是调用者
而静态的方法 ,在调用时 this 返回的是改静态方法所在的类。
class F2 {
Fproto() {
console.log('F2里的原型方法 ,this 为调用者');
console.log(this);
}
static Fstatic() {
console.log('F2 里的静态方法 , this 为该方法所处的类');
console.log(this);
return this
}
}
class S2 extends F2 {}
const s2 = new S2()
const f2 = new F2()
f2.Fproto() // F2里的原型方法 ,this 为调用者 F2 {} (F2{} 表示是 F2实例化的对象)
s2.Fproto() // F2里的原型方法 ,this 为调用者 S2 {} (同上)
F2.Fstatic() // F2 里的静态方法 , this 为该方法所处的类 [Function: F2] (表示 是 F2类)
S2.Fstatic() // F2 里的静态方法 , this 为该方法所处的类 [Function: S2]
console.log(F2.Fstatic() === F2); // true
构造函数、HomeObject 和 super()
派生类的方法可以通过 super 关键字引用它们的原型。这个关键字只能在派生类中使用,而且仅限于类构造函数、实例方法和静态方法内部。在类构造函数中使用 super 可以调用父类构造函数。
class Father{ //父类
constructor(a,b){
this.a = a,
this.b = b
},
static fl() {
console.log('form F');
}
}
class Son extends Father{
constructor(A,B,C){ //子类可以给自己添加独有的属性和方法,子类实例化对象传入的参数是直接传入这里,而不是传到父类
super (A,B) // 调用父类中的构造函数, super在此必须写在其他属性赋值之前, 相当于 super.constructor()
// 将参数传入父类,super 会将当前的 this 传给父类的构造函数中的 this。 因此此时的实例也是父类的实例
this.C = C; // this 指向的还是实例化对象,为子类添加特有的属性
}
say(){
// 如果父类有同样名的方法 , 那么子类会覆盖类
//如果子类里有同名方法,是没办法直接调用父类里该方法的,只能在方法里使用 super.say();调用父类该方法
}
static s1(){ // 子类的静态方法 , 继承了 父类 , 但是名字不一样 , 归属也不一样 。
super.fl()
}
}
几个注意点
-
构造函数的继承必须有一个 super() . 并且放子类构造函数其他代码前
-
原型方法是不默认继承的 ,如果实例调用是通过原型链访问的
-
静态方法是默认继承的 , 可以在子类调用到父类的静态方法 , 此时是相对于是子类的静态方法了 (包括 this 也指向子类)
-
如果子类没有定义 constructor , 那么父类的 constructor 就相当于是类的的构造函数 , 在实例化子类时 , 会将所有参数传给父类的构造函数
-
如果在子类定义了构造函数 , 要么必须使用 super() , 要么最后要返回一个对象。
抽象基类
有时候可能需要定义这样一个类,它可供其他类继承,但本身不会被实例化。
new.target : 保存通过 new 关键字调用的类或函数。通过在实例化时检测 new.target 是不是抽象基类,可以阻止对抽象基类的实例化。
class Class7 {
constructor() {
if (new.target === Class7) {
throw new Error('Class7 cannot be directly instantiated')
}
}
}
const c7 = new Class7 // Class7 cannot be directly instantiated
另外,通过在抽象基类构造函数中进行检查,可以要求派生类必须定义某个方法。因为原型方法在调用类构造函数之前就已经存在了,所以可以通过 this 关键字来检查相应的方法
class Class7 {
constructor() {
if (!this.foo) {
throw new Error('Inheriting class must define foo()')
}
}
}
const fc71 = class extends Class7 {
foo() {}
}
const fc72 = class extends Class7 {}
new fc71
new fc72 // Inheriting class must define foo()
继承内置类型
ES6 类为继承内置引用类型提供了顺畅的机制,开发者可以方便地扩展内置类型
(即通过继承 , 给数组 , 字符串等添加方法和属性)
class myArr extends Array {
// 不写 constructor , 实例时会自动调用Array的 constructor
// 洗牌方法
shuffle() {
// this 为数组
for (let i = 0; i <= this.length - 1; i++) {
// 随机位置
let j = Math.round(Math.random() * (this.length - 1));
// 值对调
[
[this[i]], this[j]
] = [
[this[j]], this[i]
]
}
return this
}
}
const arr = new myArr(1, 2, 3, 4, 5)
console.log(arr.shuffle()); // myArr(5) [ 5, 3, 1, 2, 4 ]
有些内置类型的方法会返回新实例对象。默认情况下,返回实例的类型与原始实例的类型是一致的
如果想覆盖这个默认行为,则可以在衍生类里覆盖静态方法: Symbol.species 访问器,这个方法返回一个类 , 这个类就是方法返回新实例时的实例类。
有些具体的代码和过程就不贴了 ,因为这个了解即可, 需要可以查
类混入
把不同类的行为集中到一个类是一种常见的 JavaScript 模式。虽然 ES6 没有显式支持多类继承,但通过现有特性可以轻松地模拟这种行为。
注意 :Object.assign()方法是为了混入对象行为而设计的。只有在需要混入类的行为时才有必要自己实现混入表达式。如果只是需要混入多个对象的属性,那么使用Object.assign()就可以了。
如希望将 A, B, C类 混入D类 。 那么可以分别将 A混入D , B混入D ,C混入D。也可以A混入B , B混入C , C混入 D;
*很多 JavaScript 框架(特别是 React)已经抛弃混入模式,转向了组合模式(把方法提取到独立的类和辅助对象中,然后把它们组合起来,但不使用继承)。这反映了那个众所周知的软件设计原则:“组合胜过继承(composition over inheritance)。”这个设计原则被很多人遵循,在代码设计中能提供极大的灵活性