关于 class 你需要知道的几件事

class 类即构造函数的语法糖,它很甜,但规则也很多

这也是一直想输出 class 相关文章,一直延期的原因

但请遵循规则,玩得才开心

# 它只是语法糖

相对 ES5 定义类的实例,原型,静态属性、方法更加便捷

同时还可以很方便继承扩展 JS 内置类,比如 Array、Map 等

但无论怎样,本质依然是函数,所以 typeof 结果为 function

函数是一等公民,所以类也是一等公民

# 特殊的调用方式

不同于 ES5 构造函数,new 不 new 都可以正常调用

但 class 类只能通过 new 调用,还可以直接使用类的 constructor 构造函数调用,构造函数依然要通过 new 调用

class A {}

// 1.
new A()

// 2.
new A.constructor()

A() // 直接调用报错
1
2
3
4
5
6
7
8
9

# 在 class 块中定义方法

class 块指的是在 {} 中定义的

类块中定义的方法作为类的原型方法被共享,而且不可枚举

即方法的描述符对象的 [[enumerable]] 值为 false

而在块外定义的都是可枚举的

另外值得注意的是类定义块内的代码都在严格模式下执行,而且类定义不可提升调用

相似的,通过 static 定义的静态方法也不可枚举

console.log(A) // 报错
// age 不可枚举,但 showAge 可枚举
class A {
  age() {}
}

A.prototype.showAge = function() {}
1
2
3
4
5
6
7

# 定义类实例、原型、静态的数据成员

两种方式,一种在类块外定义

一种在类块内定义,但不能定义原型上的数据成员

无论哪种方式定义的数据成员都是不可枚举的

// 类块外定义
class A {}
const insA = new A()
insA.a = 1
A.protottype.b = 1
A.a = 1

// 在块内直接定义,不能定义原型上的数据成员,只能通过外部定义
// 但原型上通过为共享的方法定义,不建议定义数据成员
class A {
  a = 1 // 定义在实例上
  static c = 1 // 定义在类上
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# new.target 妙用

类若只能被继承,可以通过 new.target 实现

此时的类也是抽象基类

另外说一下,new.target 指向调用者本身(类)

class A {
  constructor() {
    console.log(new.target === A) // 当 new A() 直接调用结果是 A,当通过 B 继承调用时,结果是 B
    if(new.target === A) {
      throw new Error('A 只能被继承')
    }
  }
}

class B extends A {} // ok

new A() // 报错
1
2
3
4
5
6
7
8
9
10
11
12

# 子类的构造过程

在类模式中,子类的 this 是不完整的

如果显示声明了 constructor 必须调用 super 才能完善 this 构造

且 super 必须第一个调用,调用之前不能使用 this

如果没用调用 super 必须在构造函数中返回一个自定义对象,否则报错

class A {}

class B extends A {
  constructor() {
    console.log(this) // this 是不完整的,报错
    super()
    console.log(this) // this 整理完成,ok
  }
}

new B()
1
2
3
4
5
6
7
8
9
10
11

其实说子类没有自己的 this 也是不过分的

因为它是将父类的实例属性、方法赋值给当前的上下文环境的,上下文环境通常叫 this

然后子类在这个 this 上进行修改,所以说子类的 this 是不完整的,它依赖于父类实例

所以在子类中使用 this 时一定要先使用 super,完善子类 this

# 定义一个无名称的 class

并非所有的类都有名称,当然也并非所有的函数都有名称,比如匿名函数自执行时的名称为空字符串

// 无名的类
const arr = [class {}]

console.log(arr[0].name) // "" 空字符串
1
2
3
4
// 无名的函数
~function() {
  console.log(arguments.callee.name) // ""
}()

1
2
3
4
5

# 特别的 super 关键字

super 本身是超级的意思,表示父类、超类

至于具体指父类的 constructor 构造函数、prototype 原型对象或父类本身

取决于子类使用 super 时所处的位置

它只能在子类中使用

另外 super 是关键字,独立访问报错

比如 console.log(super),就像 in 必须结合上下文才可以正常使用

  1. 在子类的 constructor 中当成函数调用,即 super() ,此时它表示父类的构造函数
  2. 在子类的原型方法当成对象使用,super.x(),此时表示父类的原型对象
  3. 在子类的静态方法当成对象使用,super.x(),此时表示父类本身
  4. 以上都是以属性查询的方式使用 spuer,但如果将 super 当成对象设置属性时,属性被设置到 this 或子类本身上,取决于 super 所处位置, 这点与心智相反
  5. delete super 上的属性报错
class A {
  getName() {}

  static getAge() {}
}

class B extends A {
  constructor() {
    super() // super 此时表示 A.constructor
    super.x = 1 // super 当成对象赋值时 super 表示子类实例对象,即 this
    delete super.x // 报错
  }

  showName() {
    super.getName() // super 此时表示 A.constructor.prototype
    super.y = 2
  }

  static showAge() {
    super.getAge() // super 此时表示 A 本身
    super.z = 3 // super 表示子类本身
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 关于父类方法中 this 指向问题

如果是父类直接 new 调用,指向父类实例或父类,

如果被继承,指向子类的实例或子类

决于 this 是在原型方法中还是在静态方法中

class A {
  getName() {
    console.log(this)
  }
  static getAge() {
    console.log(this)
  }
}

class B extends A {
  showName() {
    super.getName()
  }

  static showAge() {
    super.getAge()
  }
}

const a = new A()
a.getName() // 指向实例 a
A.getAge() // // 指向父类 A
const b = new B()
b.getName() // 继承调用,指向实例 b
B.getAge() // 继承调用,指向子类 B

b.showName() // super 调用,指向实例 b
B.showAge() // super 调用,指向子类 B

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

# 小结

以上的规则中 super 规则理解有点心智负担的

属性查询的方式使用,指向父类构造器、父类原型、父类本身,取决于使用位置

如果以赋值的方式使用,指向子类 this 实例本身、子类

类块中方法的 this 指向,非继承调用或 super 调用指向父类实例或父类本身,相反指向子类实例或子类本身

new.target 可以实现抽象继承类

并非所有类都有名称,有的可能返回是空字符串

扫一扫,微信中打开

微信二维码