Class基本语法

语法

  1. class MyClass {
  2. constructor() {...}
  3. method() {...}
  4. }
  5. // 通过new实例化,自动调用constructor,因此可以在这里初始化对象
  6. new MyClass()
  1. class User {
  2. constructor(name) {
  3. this.name = name;
  4. }
  5. sayHi() {
  6. alert(this.name);
  7. }
  8. }
  9. // 用法:
  10. let user = new User("John");
  11. user.sayHi();

注意:类的方法之间无逗号

什么是class

实际上就是函数

  1. class User {
  2. constructor(name) { this.name = name; }
  3. sayHi() { alert(this.name); }
  4. }
  5. // 佐证:User 是一个函数
  6. alert(typeof User); // function

class可以认为就是语法糖,他做了哪些?

  1. 创建一个名为User的函数,函数的代码实际上来自于 constructor 里。
  2. 类中的方法,在 prototype 里。比如上面的 User.prototype.sayHi

image.png

  1. typeof User // function
  2. User.prototype.constructor === User // true
  3. Object.getOwnPropertyNames(User.prototype) // [constructor, sayHi]

不仅仅是语法糖!

表面上看是一样,上面的代码用纯函数重写

  1. // 1. 创建构造器函数
  2. function User(name) {
  3. this.name = name;
  4. }
  5. // 函数的原型(prototype)默认具有 "constructor" 属性,
  6. // 所以,我们不需要创建它
  7. // 2. 将方法添加到原型
  8. User.prototype.sayHi = function() {
  9. alert(this.name);
  10. };
  11. // 用法:
  12. let user = new User("John");
  13. user.sayHi();

差异

必须new调用

class创建的函数,会有内部属性标记[[IsClassConstructor]]

  1. class User {
  2. constructor() {}
  3. }
  4. alert(typeof User); // function
  5. User(); // Error: Class constructor User cannot be invoked without 'new'

类上的方法不可枚举

即方法的属性描述 enumerable: false ,所以,通过for in也无法遍历出不可枚举属性。
但纯函数,除了 constructor 属性,其他原型方法都是可枚举的。

类使用user strict

类表达式

  1. // 类似 匿名函数
  2. let User = class {
  3. sayHi() {
  4. alert("Hello");
  5. }
  6. };
  7. // 也可以具名,但类的名外部不可见,内部可见
  8. let User = class My {
  9. say() {alert(My)}
  10. }
  11. new User().say() // 正常运行,打印类的字符串化内容
  12. alert(My) // error

动态创建类

有啥不可以的呢?!

  1. function makeClass(name) {
  2. return class {
  3. sayHi() {
  4. alert(name)
  5. }
  6. }
  7. }
  8. let User = makeClass('Jack')
  9. User.sayHi() // Jack

Getters/setters

类的方法本质就是在 prototype 上挂载方法,所以 setter/getter 也当然可以。

  1. class User {
  2. constructor(name) {
  3. // 调用 setter
  4. this.name = name;
  5. }
  6. get name() {
  7. return this._name;
  8. }
  9. set name(value) {
  10. if (value.length < 4) {
  11. alert("Name is too short.");
  12. return;
  13. }
  14. this._name = value;
  15. }
  16. }
  17. let user = new User("John");
  18. alert(user.name); // John
  19. user = new User(""); // Name is too short.

class字段(属性)

类可以有方法,也可以有属性(类字段),实际上就是 constructor 里初始化类属性的操作 this.name = 'jack'

  1. class User {
  2. name = 'jack'
  3. say() {alert(this.name}
  4. }
  5. new User().say() // jack

利用类字段制作绑定方法

  1. // 类方法传递时丢失this
  2. class Button {
  3. constructor(value) {
  4. this.value = value;
  5. }
  6. click() {
  7. alert(this.value);
  8. }
  9. }
  10. let button = new Button("hello");
  11. setTimeout(button.click, 1000); // undefined
  12. // 2个方式解决
  13. // 一、包装函数setTimeout(() => button.click(), 1000)
  14. // 二、类字段形式
  15. class Button {
  16. constructor(value) {
  17. this.value = value;
  18. }
  19. click = () => {
  20. alert(this.value);
  21. }
  22. }
  23. let button = new Button("hello");
  24. setTimeout(button.click, 1000); // hello
  25. // 原因:
  26. // 1. 当类进行实例化时,会创建一个新对象,并将该对象分配给this。
  27. // 2. 因此Button类实例化,click属于每个实例自身的属性
  28. // 3. 又是箭头函数,无自己的this。

例子

类写法实现一个闹钟,这个代码的精髓在于模板的替换思想,而不是根据传入的模板类型来拼接,更灵活

  1. class Clock {
  2. constructor({temp}) {
  3. this.temp = temp
  4. }
  5. render() {
  6. let date = new Date()
  7. let h = `${date.getHours()}`.padStart(2,'0')
  8. let m = `${date.getMinutes()}`.padStart(2,'0')
  9. let s = `${date.getSeconds()}`.padStart(2,'0')
  10. let output = this.temp
  11. .replace('h', h)
  12. .replace('m', m)
  13. .replace('s', s)
  14. console.log(output)
  15. }
  16. }
  17. let c = new Clock({temp: 'h:m'}) // 01:02
  18. let b = new Clock({temp: 'h:m:s'}) // 01:02:58

类继承

类继承是一个类扩展另一个类的方式。

extends关键字

  1. class Animal {
  2. constructor(name){
  3. this.name = name
  4. }
  5. run() {
  6. `${this.name} can run`
  7. }
  8. stop() {
  9. `${this.name} can stop`
  10. }
  11. }
  12. let animal = new Animal('isAnimal')

image.png

  1. // 现在有兔子,继承自animal,依旧可以使用动物类具备的方法
  2. class Rabbit extends Animal {
  3. hide() {
  4. alert(`${this.name} hides!`);
  5. }
  6. }
  7. let rabbit = new Rabbit("White Rabbit");
  8. rabbit.run() // White Rabbit can run
  9. rabbit.stop() // White Rabbit can stop

做了什么

通过 extends ,将 Rabbit.prototype 设置为了 Animal.prototype
image.png
上图中可以看到 extends 做的事,而 new 出的兔子实例对象,其 prototype 指向的是其类的原型 Rabbit.prototype
所以查找方法的路径是:查找 rabbit 实例自身 -> 查找 Rabbit.prototype -> 查找 Animal.prototype

重写方法

上面的Animal和Rabbit,后者此时如果希望有自己的stop方法,应该怎么做?因为Animal已经有stop方法了
一般来说,不希望完全替换父类方法,而是拓展或调整
因此ES6 Class提供了 super 关键字

  1. class Rabbit extends Animal {
  2. stop() {
  3. super.stop()
  4. // do others
  5. }
  6. }
  7. // 此时声明的实例,调用stop,将执行Animal类中的stop方法,再执行others

箭头函数没有super

当在箭头函数内调用 supersuper 其实是外层的 super ,如下:

  1. class Rabbit extends Animal {
  2. stop() {
  3. // 这里super是外层的,也就是Rabbit类方法stop中的,super指向父类,也就是Animal
  4. setTimeout(() => super.stop(), 1000);
  5. }
  6. }

但如果在上例中使用了普通函数,则报错: Uncaught SyntaxError: ‘super’ keyword unexpected here

  1. class Rabbit extends Animal {
  2. stop() {
  3. // 这里super并不是外部Rabbit类方法stop的,而且普通函数自己的,但是他没有继承自谁,只是普通函数,不能使用。
  4. setTimeout(function() {super.stop()}, 1000);
  5. }
  6. }

重写constructor

有点“棘手”,这里棘手是指有些规则容易造成错误。

子类无constructor

根据规范,子类没有自己的constructor的话,会默认使用父类的构造。

  1. class Son extends Parent {
  2. // 生成如下
  3. constructor(...args) {
  4. super(...args)
  5. }
  6. }

子类有constructor

必须要调用父类构造函数,其次,才能制定自己独有的属性。

  1. class Son extends Parent {
  2. constructor(name){
  3. super(name) // 第一步,必须
  4. this.x = 0 // 其次
  5. }
  6. }

内部属性[[ConstructorKind]]: “derived”

这是一个特殊内部标签,影响 new 构造实例行为。

  • new时,将创建一个新对象,且将空对象赋值给this
  • 当继承的类(子类)的constructor执行时,不会执行上述操作,而是期待父类的constructor来完成上一步的工作。

所以如下代码,没有生成新对象,没有this:

  1. class Son extends Animal {
  2. constructor(name) {
  3. this.name = name // 缺少调用父类构造,没有新对象生成,不存在this
  4. }
  5. }

报错信息:Uncaught ReferenceError: Must call super constructor in derived class before accessing ‘this’ or returning from derived constructor

重写类字段

父类和子类的构造调用 和 初始化(比如 this.name = name 这类属性初始化)的顺序不一样。
父类是先调用构造函数,再初始化。子类恰好相反,子类在 super 调用后再初始化,这种顺序差异,会导致字段被父类构造器使用时会出现异常!

  1. class Animal {
  2. name = 'animal';
  3. constructor() {
  4. alert(this.name); // 父类构造函数中访问了this.name
  5. }
  6. }
  7. class Rabbit extends Animal {
  8. name = 'rabbit'; // 子类重写了name字段。但如上所说,父类构造函数在被子类继承,执行super时,子类的name = 'rabbit'还没运作,所以访问的是父类的name
  9. }
  10. new Animal(); // animal
  11. new Rabbit(); // animal

super的内部机制

比如super如何知道是调用父类的方法?难道是 son.__proto__.fn?

  1. // 原型链来实现super的设想,有很大问题,会造成无限循环查找
  2. let animal = {
  3. name: 'animal',
  4. eat() {
  5. this.name
  6. }
  7. }
  8. let rabbit = {
  9. __proto__: animal,
  10. name: 'Rabbit',
  11. eat() {
  12. // super.eat的工作方式,猜想利用原型链
  13. // 这里需要call(this),否在this.__proto__.eat(),this变为原型animal了
  14. this.__proto__.eat.call(this)
  15. }
  16. }
  17. rabbit.eat() // Rabbit 调用原型对象里的eat方法,且绑定了this指向rabbit,看起来没问题

但如果在原型链上在加一个对象,就有问题了。

  1. let longEar = {
  2. __proto__: rabbit,
  3. eat() {
  4. this.__proto__.eat.call(this)
  5. }
  6. }
  7. longEar.eat()
  8. // this此时是longEar
  9. // 所以longEar.__proto__.eat.call(longEar)
  10. // => rabbit.eat.call(longEar)
  11. // 在rabbit中的eat如下
  12. // longEar.__proto__eat.call(longEar)
  13. // => rabbit.eat.call(longEar) 这里绕回来了,死循环!

揭秘

内部使用了 `[[HomeObject]]`` ,一个内部属性,当一个函数被定义为类或者对象方法时,该属性绑定为该对象

  1. let animal = {
  2. name: "Animal",
  3. eat() { // animal.eat.[[HomeObject]] == animal
  4. alert(`${this.name} eats.`);
  5. }
  6. };
  7. let rabbit = {
  8. __proto__: animal,
  9. name: "Rabbit",
  10. eat() { // rabbit.eat.[[HomeObject]] == rabbit
  11. super.eat();
  12. }
  13. };
  14. let longEar = {
  15. __proto__: rabbit,
  16. name: "Long Ear",
  17. eat() { // longEar.eat.[[HomeObject]] == longEar
  18. super.eat();
  19. }
  20. };
  21. // longEar.eat() this指向longEar
  22. // => super.eat 内部:这里使用了super,所以查找longEar.eat.[[HomeObject]]的原型里的eat方法,longEar.eat.[[HomeObject]]指向longEar,它的原型是rabbit
  23. // rabbit.eat()
  24. // => super.eat 内部:这里使用了super,查找rabbit.eat.[[HomeObject]]的原型里的eat方法,rabbit.eat.[[HomeObject]]指向rabbit,它的原型是animal
  25. // animal.eat()
  26. // => this.name,因为是longEar.eat(),所以this是longEar,打印Long Ear eats.

方法不是自由的了

因为有 super 的存在,可以绑定一个上下文,且不可修改。它没有使用 this 的机制,调用时获取上下文。

只使用方法!

super 只能用于方法,函数属性中使用会报错!

  1. let animal = {
  2. eat: function() {} // 对象的 函数属性 eat
  3. }
  4. let animal = {
  5. eat() {} // 对象的方法 eat
  6. }

静态属性和静态方法

static

通过关键字 static 来声明静态属性和方法

  1. class Animal {
  2. static type = 'animal'
  3. }
  4. Animal.type // animal
  5. // 效果同:
  6. class Animal {}
  7. Animal.type = 'animal'

静态方法用法

工厂方法?

  1. class Animal {
  2. constructor(name) {
  3. this.type = `${name} is am animal`
  4. }
  5. static createAnimal(name) {
  6. return new this(name)
  7. }
  8. }
  9. let a = Animal.createAnimal('rabbit')
  10. a.title // rabbit is am animal

可继承

静态方法属性相当于在类上直接挂在(函数的话,就是函数上直接挂)

  1. class Aniaml {
  2. static type = 'animal'
  3. say() {}
  4. }
  5. class Rabbit extends Animal {}
  6. // extends的关联了2个原型!!!
  7. // Rabbit.__proto__ === Aniaml
  8. // Rabbit.__proto__.__proto__ === Animal.prototype
  9. // 所以
  10. Rabbit.type // animal
  11. Rabbit.prototype.say // say() {}

巩固

  1. // 说好的JS中万物皆对象(所有对象都继承自Object.prototype),那下面2者有区别?
  2. class Animal extends Object {}
  3. class Animal {}
  4. // 有区别
  5. // 前者:
  6. Aniaml.__proto__ === Object
  7. Animal.prototype.__proto__ === Object.prototype
  8. // 后者:
  9. Animal.__proto__ === Function.prototype
  10. // class本质也是函数,所有函数的原型都继承自Function.prototype,就像所有的数组继承自Array.prototype,数字继承自Number.prototype一样。

私有的和受保护的属性和方法

受保护的

JS中约定以_开头,允许类自身,继承的类的内部来方法。但目前JS没有提供语言上的能力保障受保护的字段,也就是说,可以直接修改它。

  1. class Parent {
  2. _smoke = true
  3. }
  4. let p = new Parent()
  5. p._smoke = false; // ok的

私有的

JS语言级支持,以 # 开头。不能从类的外部(实例)访问,继承的类也不行。image.png
不支持通过 this['#smoke'] 来访问,语法层面限制,只能是 this.#smoke

扩展内建类

内建类可以扩展

扩展后的内建类具有【传递性】。

  1. class PowerArray extends Array {
  2. isEmpty() {
  3. return this.length = 0
  4. }
  5. }
  6. let arr = new PowerArray(1,2,3)
  7. arr.isEmpty() // false
  8. //arr也可以继续使用内建Array的原型方法
  9. let filterArr = arr.filter(item => item >= 2) // [2,3]
  10. filterArr.isEmpty() // false
  11. // 这里很有趣,注意,filter,map等返回的新数组,使用的是 arr.constructor 也就是 PowerArray来生成新的数组的。所以filterArr 依旧是 PowerAarry 的实例。
  12. filterArr instanceof PowerArray // true
  13. filterArr.__proto__ === PowerArray.prototype // true

如果不想有这种传递行为,希望 filter map 使用内建 Array 来生成的话,利用 Symbol.species ,一个特殊的静态属性,当调用 filter map 这类内建方法时,会使用该静态属性指定的内(构造函数)

  1. class PowerArray extends Array {
  2. isEmpty() {
  3. return this.length === 0;
  4. }
  5. // 内建方法将使用这个作为 constructor
  6. static get [Symbol.species]() {
  7. return Array;
  8. }
  9. }
  10. let arr = new PowerArray(1, 2, 5, 10, 50);
  11. alert(arr.isEmpty()); // false
  12. let filterArr = arr.filter(item => item >= 10)
  13. // filter 触发Symbol.species
  14. fitlerArr.isEmpty() // isEmpty is not a function 。看不生效了

内建类没有继承静态方法

静态属性和静态方法一节里,有说过静态方法和属性的继承是这么实现的

  1. class A {
  2. static type = 'A'
  3. }
  4. class B extends A {}
  5. let b = new B() // 注意不是实例b.type === 'A'
  6. B.type === 'A' // true

JS中一切都是对象,所以Array这些扩展自Object。复习一下:

  1. Array.prototype.__proto__ === Object.prototype // true
  2. Array.__proto__.__proto__ === Object.prototype // true
  3. // 看来Array.prototype === Array.__proto__ // false
  4. // 错!
  5. // 函数的原型都是继承自函数原型对象
  6. Array.__proto__ === Function.prototype // true
  7. // 而对象都继承自Object原型,prototype是函数的一个普通属性(没什么特别的,声明时就有了,可以被改写,覆盖)
  8. // 所以可以如下
  9. Function.prototype.__proto__ === Object.prototype
  10. // 而下面,仅仅是Array的一个普通对象,是所有数组实例的原型对象。
  11. Array.prototype

Array.key 并不存在,所以Array并没有继承Object的静态方法,同样, Date.key 之类的静态方法也不存在。
image.png

类检查

instanceof

语法: obj instancof class ,也包括了原型链检查。

  1. class A {}
  2. let a = new A()
  3. a instanceof A // true
  4. class B extends A {} // 这里B.prototype.__proto__ === A.prototype
  5. let b = new B()
  6. b instanceof B // true 因为b.__proto__ == B.prototype。
  7. b instanceof A // true 因为b.__proto__.__proto__ === A.protoype

Symbol.hasInstance

instanceof的检查,会去调用该方法,如果没有该方法,则走标准的逻辑:检查 class.protoype是否是obj的原型链中的原型之一
我们来看下有 hasInstance

  1. class Animal {
  2. static [Symbol.hasInstance](obj) {
  3. if(obj.canEat) {return true}
  4. }
  5. }
  6. // 只是声明了一个跟Animal毫无关系的普通对象
  7. let obj = {canEat: true}
  8. obj instanceof Animal // true 因为触发了hasInstance

isPrototypeOf

instanceof 类似,也会检查原型链上是否有。语法: class.protoype.isPrototypeOf(obj)

最安全的Object.prototype.toString

调用则抽取成如下结构: [object Number]

  1. Object.prototype.toString.call([]) === '[object Array]'

Mixin

mixin 是可被其他类使用,但无需继承的方法的类。
JS里面原型链模式规定,每个对象只能有一个原型对象,每个类只可以扩展另外一个类,也就是单继承。

一个Mixin实例

构造一个拥有实用方法的对象,复制到需要拓展的类的原型中。

  1. // 这是一个mixin,根据维基百科的定义,mixin也是一个类
  2. let sayHiMixin = {
  3. sayHi() {
  4. alert(`Hello ${this.name}`);
  5. },
  6. sayBye() {
  7. alert(`Bye ${this.name}`);
  8. }
  9. };
  10. // 用法:
  11. class User {
  12. constructor(name) {
  13. this.name = name;
  14. }
  15. }
  16. // 拷贝方法
  17. Object.assign(User.prototype, sayHiMixin);
  18. // 现在 User 可以打招呼了
  19. new User("Dude").sayHi(); // Hello Dude!

mixin内部继承

  1. let sayMixin = {
  2. say(name) {
  3. alert(name)
  4. }
  5. }
  6. let sayHiMixin = {
  7. __proto__: sayMixin // 这里设置了sayHiMixin的原型对象
  8. sayHi() {
  9. super.say(`Hi ${this.name}`)
  10. }
  11. }
  12. class User {
  13. constructor(name){
  14. this.name = name
  15. }
  16. }
  17. User.__proto__ = sayHiMixin
  18. new User('Jack').sayHi()
  19. // User.prototype ->
  20. // 找到sayHi ->
  21. // 利用[HomeObject]找到其sayMixin 中的 say方法。

注意mixin 同名方法造成的冲突,因此需要注意命名。