细读 ES6 | Class 下篇

配图源自 Freepik

上一篇介绍了 Class 的语法,今天来看看 ES6 中的继承。

在 ES5 大概有 6 种继承方式:类式继承、构造函数继承、组合式继承、原型继承、寄生式继承、寄生组合式继承,而这些方式都有一些各自的缺点,可看文章:深入 JavaScript 继承原理

而 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 同时是 StudentPerson 类的实例,这点与 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 并没有继承自其他类,因此在父类 Personconstructor() 方法内调用是会报错的。

  • 调用 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.xxxsuper.xxx())相当于 Parent.prototype.xxxParent.prototype.xxx.call(this))。

  • 作为对象时,若在静态方法内使用,super.xxxsuper.xxx())相当于 Parent.xxxParent.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,子类的 Studentprototype 属性的 __proto__ 属性指向父类 Personprototype 属性。

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

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.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,794评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,050评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,587评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,861评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,901评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,898评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,832评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,617评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,077评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,349评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,483评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,199评论 5 341
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,824评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,442评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,632评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,474评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,393评论 2 352

推荐阅读更多精彩内容