zl程序教程

您现在的位置是:首页 >  后端

当前栏目

细读 ES6 | Class 下篇

ES6 Class 下篇
2023-09-27 14:25:56 时间
前言

今天来看看 ES6 中的继承。


在 ES5 大概有 6 种继承方式 类式继承、构造函数继承、组合式继承、原型继承、寄生式继承、寄生组合式继承 而这些方式都有一些各自的缺点


而 ES6 标准提供了 Class 类 的语法来定义类 语法很像传统的面向对象写法。本质上仍然是通过原型实现继承的 可以理解为 class 只是一个语法糖 跟传统面向对象的类又不一样。废话说完了 入正题...


那 Class 是怎样实现继承的呢


正文一、简介


通过 extends 关键字实现继承 比 ES5 写一长串的原型链 方便清晰很多 对吧。

class Person {}

class Student extends Person {} // 没错 这样就实现了继承


上面的示例中 定义了一个 Student 子 类 该类通过 extends 关键字继承了 Person 父 类的所有属性和方法。但由于两个类中并没有实现什么功能 相当于 Student 复制了一个 Person 类而已。


需要注意的是 若子类自实现了 constructor 方法 需在其内部使用 super 关键字来调用父类的构造方法 否则当子类进行实例化时会报错。

class Person {}

class Student extends Person {

 constructor() {

 super()

 // 相当于调用父类 Person 的构造方法 

 // 相当于 Person.prototype.constructor.call(this) 

 // 而且 若 constructor 内使用了 this 关键字 

 // super() 一定要在 this 之前进行调用 否则会报错。

const stu new Student() // ReferenceError: Must call super constructor in derived class before accessing this or returning from derived constructor

// 至于为什么上一个示例 实例化时并不会报错 原因如下 

// 当 constructor 缺省时 JS 引擎会默认添加一个 constructor 方法 相当于 

// class Person {}

// class Student extends Person {

// constructor(...args) {

// super(...args)

// }


原因是 ES5 的继承实质是先创建子类的实例对象 即 this 然后再将父类的方法添加到实例对象 this 上 类似 Parent.apply(this) 。而 ES6 继承机制完全不同 它先将父类的实例对象的属性和方法 放到 this 上 然后再用子类的构造函数修改 this。因此 在子类使用 this 之前 需要调用 super() 方法执行父类的 constructor() 方法来创建实例对象 this。


我们来改下

class Person {

 constructor(name, age) {

 this.name name

 this.age age

 sayHi() {

 console.log( Hi, my name is ${this.name}. )

class Student extends Person {

 constructor(name, age, stuNo) {

 super(name, age)

 this.stuNo stuNo // this 只能在调用 super() 后使用

const stu new Student( Frankie , 20, 2021001)

stu.sayHi() // Hi, my name is Frankie. 


实例对象 stu 同时是 Student、Person 类的实例 这点与 ES5 表现一致。

stu instanceof Student // true

stu instanceof Person // true


二、Object.getPrototypeOf()


使用 Object.getPrototypeOf() 可以通过子类获取其直接父类。

Object.getPrototypeOf(stu) Student // true

Object.getPrototypeOf(Student) Person // true

// 也可以使用非标准的 __proto__ 访问原型

stu.__proto__.constructor Student // true

stu.__proto__ Student.prototype // true


提一下 我们一直使用的 Object.prototype.__proto__ 并不是 ECMAScript 标准 只是被各大浏览器厂商支持 因此我们才可以使用。现在被推荐使用的是 标准支持的 Objec.getPrototypeOf()、Object.setPrototypeOf() 方法。


三、super 关键字


关键字 super 可以作为函数使用 也可以作为对象使用 两种是有区别的。

1. super 作为函数

当 super 作为函数 只能在 子类 构造方法中使用 若在非子类或类的其他方法中调用 是会报错的。

class Person {

 constructor(name, age) {

 this.name name

 this.age age

 console.log(new.target.name)

 // super() // 若在此调用 会报错 SyntaxError: super keyword unexpected here

 sayHi() {

 console.log( Hi, my name is ${this.name}. )

class Student extends Person {

 constructor(name, age, stuNo) {

 // super(name, age)

 console.log(super(name, age) this) // true

 this.stuNo stuNo // this 只能在调用 super() 后使用

 getStuNo() {

 // super() // 若在此调用 会报错 SyntaxError: super keyword unexpected here

 return this.stuNo

const stu new Student( Frankie , 20, 2021001) // 打印 Student 

const person new Person( Mandy , 18) // 打印 Person 


在构造方法当作函数调用 super() 它代表了父类的构造方法。根据 ES6 规定 子类的构造函数必须执行一次 super 函数。当缺省 constructor() 方法时 JS 引擎会帮我们添加一个默认构造方法 里面也包括 super() 的调用。


小结


super() 只能在子类的 constructor() 方法内调用 在 getStuNo() 方法内调用会报错。例如 示例中父类 Person 并没有继承自其他类 因此在父类 Person 的 constructor() 方法内调用是会报错的。
调用 super() 返回当前实例化对象 即 this。
new.target 指向直接被 new 执行的类。因此通过 new Student() 和 new Person() 进行实例化时 new.target 分别指向 Student 类和 Person 类。


2. super 作为对象


当 super 作为对象使用时 在普通方法内 包括 constructor() 在内的非静态方法 它指向父类的原型对象 即 Parent.prototype 而在静态方法内 它指向父类 即 Parent 。

// 父类

class Person {

 constructor(name, age) {

 this.name name

 this.age age

 sayHi() {

 console.log( Hi, my name is ${this.name}. )

 printAge() {

 console.log(this.age)

 static classMethodParent() {

 console.log(Person.name)

Object.assign(Person.prototype, {

 prop: hhh 

// 子类

class Student extends Person {

 constructor(name, age, stuNo) {

 super(name, age)

 this.stuNo stuNo

 // 作为对象 super 相当于 Person.prototype

 super.sayHi() // Hi, my name is Frankie. 

 console.log(super.name) // undefined

 console.log(super.prop) // hhh 

 // 另外 要注意 

 super.tmp temporary // 相当于 this.tmp temporary 

 console.log(super.tmp) // undefined

 console.log(this.tmp) // temporary 

 getStuNo() {

 return this.stuNo

 getAge() {

 // 普通方法内 super 指向父类的原型对象 

 // 即相当于 Person.prototype.printAge.call(this)

 super.printAge()

 static classMethod() {

 // 静态方法内 super 指向父类

 // 相当于 Person.classMethodParent()

 super.classMethodParent()

const stu new Student( Frankie , 20, 2021001)

stu.getAge() // 20

Student.classMethod() // Person 


小结


在普通方法内 类似 super.xxx 等取值操作 super 均指向父类的原型对象。例如 上述子类构造方法内 super.name 打印结果为 undefined 原因是属性 name 是挂载到实例对象上的 而不是实例的原型对象上的。即 super.name 相当于 Person.prototype.name。
在普通方法内 类似 super.xxx xxx 等赋值操作 相当于 this.xxx xxx 因此属性 xxx 会被挂载到实例对象上 而不是父类原型对象。
普通方法内 通过 super.xxx() 调用父类方法 相当于 Person.prototype.xxx.call(this)。
在静态方法内 super 指向父类 因此 super.xxx() 相当于 Person.classMethodParent()。


以上提到的普通方法 是指非静态方法。


3. 注意事项

无论 super 是作为函数 还是对象使用 必须明确指定 否则会报错。

class Person {}

class Student extends Person {

 constructor() {

 super()

 console.log(super) // SyntaxError: super keyword unexpected here

}


4. 关于 super 总结


作为函数时 仅可在子类的 constructor() 内使用。若 constructor() 内包括 this 的使用 则 super() 必须在 this 之前进行调用。
作为对象时 若在非静态方法内使用 super.xxx super.xxx() 相当于 Parent.prototype.xxx Parent.prototype.xxx.call(this) 。
作为对象时 若在静态方法内使用 super.xxx super.xxx() 相当于 Parent.xxx Parent.xxx() 。
我们都知道在 JavaScript 访问对象的某个属性 或方法 先从对象本身去查找是否有此属性 再从原型上一层一层的查找 若最终查找不到会返回 undefined 或抛出 TypeError 错误 。
同样地 在 Class 继承中 若子类、父类存在同名方法 使用实例对象进行调用该方法 若子类查找到了 自然不会再去父类中查找。但我们在设计类的时候 可能仍需要执行父类的同名方法 那么怎么调用呢
显然通过 父类名.方法名() 的方式调用是不合理、不灵活的 道理就跟 JavaScript 要设计 this 关键字一样。于是 super 就诞生了 最后这句是我猜的 哈哈 。


四、类的 prototype 属性和 __proto__ 属性


在 ES5 之中 每个对象都有 __proto__ 属性 它指向对象的构造函数的 prototype 属性。

关于对象可分为 普通对象和函数对象 区别如下


对象类型prototype 原型对象 __proto__ 原型 普通对象❌✅函数对象✅✅


所有对象都有 __proto__ 属性 而只有函数对象才具有 prototype 属性。其中构造函数属于函数对象 而实例对象则属于普通对象 因此实例对象是没有 prototype 属性的。


而 ES6 的 class 作为构造函数的语法糖 同时有 prototype 属性和 __proto__ 因此同时存在两条继承链。


子类的 __proto__ 属性 表示构造函数的继承 总是指向父类。子类的 prototype 属性的 __proto__ 属性 表示方法的继承 总是指向父类的 prototype 属性。

class Person { }

class Student extends Person { }

Student.__proto__ Person // true

Student.prototype.__proto__ Student.prototype // true


上面的示例中 子类 Student 的 __proto__ 指向父类 Person 子类的 Student 的 prototype 属性的 __proto__ 属性指向父类 Person 的 prototype 属性。

因为类的继承 是按照以下模式实现的

class Person { }

class Student extends Person { }

// 相当于

class Person { }

class Student { }

Object.setPrototypeOf(Student, Person)

Object.setPrototypeOf(Student.prototype, Person.prototype)

// -------------------------------------------------------

// 关于 Object.setPrototypeOf() 内部是这样实现的 

// Object.setPrototypeOf functon(obj, proto) {

// obj.__proto__ proto

// return obj

// 因此 相当于 

// Student.__proto__ Person

// Student.prototype.__proto__ Person.prototype

// -------------------------------------------------------


这样去理解


作为一个对象 子类 Student 的原型 __proto__ 是父类 Person 作为一个构造函数 子类 Student 的原型对象 prototype 是父类 Person 的原型对象 prototype 的实例。


因此 理论上 extends 关键字后面的 函数 对象 只要含有 prototype 属性就可以了 但 Function.prototype 除外。但在做项目的时候应该从实际应用场景考虑 这样去做是否有意义。

// 1. Student 类继承 Object 类

class Student extends Object { }

Student.__proto__ Object // true

Student.prototype.__proto__ Object.prototype // true

// 2. Student 不继承

class Student { }

Student.__proto__ Function.prototype

Student.prototype.__proto__ Object.prototype

// 因此 相当于 

// Object.setPrototypeOf(Student, Function.prototype)

// Object.setPrototypeOf(Student.prototype, Object.prototype)


五、Mixin 模式的实现


Mixin 指的是多个对象合成一个新的对象 新对象具备各个组成成员的接口。它的最简单实现如下

const a { a: 1 }

const b { b: 2 }

const c { ...a, ...b }


上面的示例 对象 c 是对象 a 和对象 b 的合成 具备两者的接口。

下面是一个更完备的实现 将多个类的接口“混入”另一个类。

function mix(...mixins) {

 class Mix {

 constructor() {

 for (let mixin of mixins) {

 copyProperties(this, new mixin()) // 拷贝实例属性

 for (let mixin of mixins) {

 copyProperties(Mix, mixin) // 拷贝静态属性

 copyProperties(Mix.prototype, mixin.prototype) // 拷贝原型属性

 return Mix

function copyProperties(target, source) {

 for (let key of Reflect.ownKeys(source)) {

 if (

 key ! constructor 

 key ! prototype 

 key ! name 

 let desc Object.getOwnPropertyDescriptor(source, key)

 Object.defineProperty(target, key, desc)

}


上面示例中的的 mix 函数 可以将多个对象合成为一个类。使用的时候 只要继承这个类即可。

class DistributedEdit extends mix(Loggable, Serializable) {

 // ...

}


The end.