ES6 class

class 之前,我们一般使用 "构造函数" 构建对象。现在有了 class,我们可以更好地进行面向对象编程。

这是一个简单的 ES5 构造函数:

function User(name, gender) {
  this.name = name
  this.gender = gender
}

const tom = new User('Tom', 'male')

Notice

在 JavaScript 中,所谓的构造函数与其他语言如 Java 并无相同之处,仅仅是因为该函数被 new 调用才叫做构造函数。

这是 ES6 class 版本的:

class User {
  constructor(name, gender) {
    this.name = name
    this.gender = gender
  }
}
const tom = new User('Tom', 'male')

class 实际上是一种特殊的函数,就像你能够定义的函数表达式和函数声明一样,类语法有两个组成部分:类表达式和类声明。

// 类声明
class User {}
// 类表达式
const User = class {}
// 类也是函数
console.log(typeof User) // function
// 并且类本身就指向构造函数
User === User.prototype.constructor // true

Notice

类和函数有一个重要的不同就是:函数存在声明提升,而类不会,你必须在类声明了之后再实例化。

实例的属性除非显式定义在其本身(即定义在 this 对象上),否则都是定义在原型上(即定义在 class 上):

//定义类
class Point {
  constructor(x, y) {
    this.x = x
    this.y = y
  }
  toString() {
    return '(' + this.x + ', ' + this.y + ')'
  }
}
var point = new Point(2, 3)
point.toString() // (2, 3)
point.hasOwnProperty('x') // true
point.hasOwnProperty('y') // true
point.hasOwnProperty('toString') // false
point.__proto__.hasOwnProperty('toString') // true

构造函数

constructor 方法用于创建和初始化一个由 class 创建的对象,当通过 new 调用时,会自动调用该方法。

Notice

一个类必须拥有 constructor 方法!如果没有显式定义,则会默认添加一个返回当前实例对象 thisconstructor

getter&setter

与 ES5 一样,在 class 的内部可以使用 getset 关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为。

class MyClass {
  constructor() {
    // ...
  }
  get prop() {
    return 'getter'
  }
  set prop(value) {
    console.log('setter: ' + value)
  }
}
let inst = new MyClass()
inst.prop = 123 // setter: 123
inst.prop // 'getter'

static

静态方法调用直接在类上进行,不能在类的实例上调用。静态方法通常用于创建实用程序函数,静态方法不会被子类继承

class User {
  static staticMethod() {
    console.log('This is a static method')
  }
}
const Tom = new User()
Tom.staticMethod() // error: Tom.staticMethod is not a function
User.staticMethod() // log: This is a static method

静态方法调用同一个类中的其他静态方法,可使用 this 关键字,但在非静态方法中,不能直接使用 this 关键字来访问静态方法。而是要用类名来调用:

class User {
  constructor() {
    this.method()
  }
  method() {
    User.staticMethod1()
  }
  static staticMethod1() {
    this.staticMethod2()
  }
  static staticMethod2() {
    console.log('This is static method 2')
  }
}
new User() // log: This is static method 2

静态属性(ES7):同 ES6 的静态方法声明方式,可以在 Class 内部声明,声明方式为在属性声明前加上 static 关键字:

//ES6:
class Foo {}
Foo.prop = 1 //静态属性(类的属性)

//ES7:
class Foo {
  static prop = 1 //静态属性
}

class MyClass {
  static myStaticProp = 42
  constructor() {
    console.log(MyClass.myProp) // 42
  }
}

类的 prototype 属性和 __proto__ 属性

大多数浏览器的 ES5 实现之中,每一个对象都有 __proto__ 属性,指向对应的构造函数的 prototype 属性。Class 作为构造函数的语法糖,同时有 prototype 属性和 __proto__ 属性,因此同时存在两条继承链。

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

super

super 可以当作函数使用:

class Parent {}
class Child extends Parent {
  constructor() {
    super()
  }
}

ES5 的继承,实质是先创造子类的实例对象 this,然后再将父类的方法添加到 this 上面,而在 ES6 中,子类想要继承父类,必须调用 super,实质是先将父类实例对象的属性和方法,加到 this 上面(所以必须先调用 super 方法),然后再用子类的构造函数修改 this。因此,当你在子类构造函数中操作 this 的代码必须写在 super 后面。

执行了 super 就是执行了父类构造函数,但 super 内部的 this 指向的是子类,super 执行后返回的也是子类的实例,因此相当于执行 Parent.prototype.constructor.call(this)

super 也可以调用父类上的静态方法,但只有在子类的静态方法上才可以这么做,非静态方法中还是要通过父类名字调用静态方法:

class Parent {
  constructor() {}
  method() {
    console.log('This is a method from parent')
  }
  static staticMethod() {
    console.log('This is a staticMethod from parent')
  }
}
class Child extends Parent {
  constructor() {
    super()
    super.method()
  }
  static staticChildMethod() {
    super.staticMethod()
  }
}
new Child() // This is a method from parent
Child.staticChildMethod() // This is a staticMethod from parent

可以在子类的静态方法中调用父类的静态方法,此时 super 指向的是父类本身。在子类普通方法和 constructor 中可以调用父类的普通方法,此时 super 指向的是父类的原型。

通过 super 调用父类方法时,super 会绑定子类的 this

class Parent {
  constructor() {
    this.x = 1
  }
  method() {
    console.log(this.x)
  }
}
class Child extends Parent {
  constructor() {
    super()
    this.x = 2
    super.method()
  }
}
new Child() // log: 2

因此,如果使用 super 对属性赋值,那么改变的将是子类的属性而不是父类属性:

class Parent {
  constructor() {
    this.x = 1
  }
}
class Child extends Parent {
  constructor() {
    super()
    this.x = 2
    super.x = 3
    console.log(this.x) // 3
  }
}
new Child()

直接输出 super 是会报错的,因为 JS 引擎不知道 super 是作为函数调用还是作为对象使用。

class Parent {}
class Child extends Parent {
  constructor() {
    console.log(super) // error
  }
}

super 只能在类中使用么?

不是,super 还可以在对象字面量中使用:

const obj1 = {
  method1() {
    console.log("method 1");
  }
}

const obj2 = {
  method2() {
   super.method1();
  }
}

Object.setPrototypeOf(obj2, obj1);
obj2.method2(); // logs "method 1"

公共字段

可以在类中声明公共字段。

class Rectangle {
  height = 0 // 初始化的公共字段
  width // 未初始化的公共字段
  static bgc = '#FFFFFF' // 静态公共字段
  constructor(height, width, bgc) {
    this.height = height
    this.width = width
    Rectangle.bgc = bgc
  }
}
const rectangle = new Rectangle(12, 21)
console.log(rectangle.height) // log: 12

私有字段

私有字段使用 # 开头,只能在类的内部进行调用,而不能在类之外引用。

class Rectangle {
  #height = 0
  #width
  constructor(height, width) {
    this.#height = height
    this.#width = width
  }
  getHeight() {
    return this.#height
  }
}
const rectangle = new Rectangle(12, 21)
console.log(rectangle.#height) // error: Private field '#height' must be declared in an enclosing class
console.log(rectangle.getHeight()) // log: 12

除了这个新特性之外,还有几种方法可以实现:

class SimCard {
  constructor(number, type, pinCode) {
    this.number = number
    this.type = type
    let _pinCode = pinCode
    // this property is intended to be a private one
    this.getPinCode = () => {
      return _pinCode
    }
  }
}
const card = new SimCard('444-555-666', 'Nano SIM', 1515)
console.log(card._pinCode) // outputs undefined
console.log(card.getPinCode()) // outputs 1515

在 JS 界约定俗成使用 _ 开头作为私有属性,然后配合 getter 实现私有属性机制。

也可以使用 Symbol 定义唯一属性来实现:

const SimCard = (() => {
  const _pinCode = Symbol('PinCode')
  class SimCard {
    constructor(number, type, pinCode) {
      this.number = number
      this.type = type
      this[_pinCode] = pinCode
    }
    get pinCode() {
      return this[_pinCode]
    }
  }
  return SimCard
})()
const card = new SimCard('444-555-666', 'Nano SIM', 1515)
console.log(card._pinCode) // outputs undefined
console.log(card.pinCode) // outputs 1515

不过外部仍然可以使用 Object.getOwnPropertySymbols(SimCard) 来获取使用 Symbol 定义的属性。

new.target

new.target 属性允许你检测函数和或者构造方法是否是通过 new 被调用的,如果是 new 调用的,new.target 就会返回一个指向构造方法或函数的引用。在普通函数调用中,new.target 的值是 undefined

function User() {
  if (new.target === undefined) {
    console.log('没有使用new调用')
  } else {
    console.log(new.target === User) // true
  }
}

但在有子类继承父类时,实例化子类时 new.target 会返回子类,因此可以用来写出不能独立使用、必须继承后才能使用的类:

class Parent {
  constructor(){
    if(new.target === Parent) {
      throw new Error('本类不能被实例化!')
    }
  }
}
class Child extends Parent {
  constructor{
    super()
  }
}
new Parent() // 报错:本类不能被实例化!
new Child() // 正确

内部实现

我们知道 JavaScript 中所谓的 class 不过是一个语法糖而已,与 Java 的 class 完全不同。那么 JavaScript 的 class 是如何实现的呢?

在此之前,如果不明白 new 做了什么,可以看 new 绑定

我们知道在 ES5 中经过 new 调用的构造函数产生的实例会把 __proto__ 指向构造函数的 prototype,如果是 ES6 的 class 呢?

class User {
  constructor(name) {
    this.name = name
  }
}
const tom = new User('Tom')
console.log(tom.__proto__ === User.prototype) // true
console.log(tom.__proto__.constructor === User) // true

因此,new 调用构造函数和类产生的结果都是相同的。只是一个指向构造函数本身,一个指向类。

我们在 ES5 中可以使用原型链继承属性和方法:

function User(name) {
  this.name = name
}
User.prototype.getName = function () {
  return this.name
}
const tom = new User('tom')
console.log(tom.getName()) // log: tom

上面的代码在 ES6 class 中可以等同于:

class User {
  constructor(name) {
    this.name = name
  }
  getName() {
    return this.name
  }
}
const tom = new User('tom')

箭头函数和普通函数

我们知道类中可以定义箭头函数也可以定义普通函数:

class A {
  constructor() {}
  b() {}
  c = () => {}
}

但实际上两者完全不同,我们来看一个例子:

class A {
  constructor() {}
  b() {}
  c = () => {}
}
const d = new A()
const e = new A()
console.log(d.b === e.b) // true
console.log(d.c === e.c) // false

在类中箭头函数的定义实际上会变成:

class A {
  constructor() {
    this.c = () => {}
  }
}
A.prototype.b = function () {}

也就是说,箭头函数和普通属性的行为一样,每创建一个子类都会重新定义,而普通函数则是定义在类的原型上的。

总结

class 的本质还是 ES5 的原型链和构造函数,但是比原来的语法更加方便。class 没有声明提升,也不能覆写,但是函数有声明提升,也可以覆写。

参考文章