ES6 新特性之类(Class)和继承(extends)

简介

JavaScript 语言中,生成实例对象的传统方法是通过构造函数

function Point(x, y) {
  this.x = x
  this.y = y
}
Point.prototype.toString = function () {
  return '(' + this.x + ', ' + this.y + ')'
}
let p = new Point(1, 2)

ES6 提供了更接近传统语言的写法,引入了 Class(类)这个概念,上面的代码用 ES6 的 class 改写,就是下面这样

class Point {
  constructor (x, y) {
    this.x = x
    this.y = y
  }
  toString () {
    return '(' + this.x + ', ' + this.y + ')'
  }
}
注意:定义“类”的方法的时候,前面不需要 function 这个关键字,另外,方法之间不需要逗号分隔

ES6 的类,完全可以看作构造函数的另一种写法

class Point {
  // ...
}
typeof Point // "function"
Point === Point.prototype.constructor // true

使用的时候,也是直接对类使用 new 命令,跟构造函数的用法完全一致

class Bar {
  doStuff () {
    console.log('stuff')
  }
}
let b = new Bar()
b.doStuff() // "stuff"

constructor 方法

constructor 方法是类的默认方法,通过 new 命令生成对象实例时,自动调用该方法,如果没有显式定义,一个空的 constructor 方法会被默认添加

class Point {
}

// 等同于
class Point {
  constructor () {}
}

getter 和 setter

与 ES5 一样,在“类”的内部可以使用 get 和 set 关键字,对某个属性设置存值函数和取值函数

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 Foo {
  static classMethod () {
    return 'hello'
  }
}

Foo.classMethod() // 'hello'

let foo = new Foo()
foo.classMethod()
// TypeError: foo.classMethod is not a function、

如果静态方法包含 this 关键字,这个 this 指的是类,而不是实例

class Foo {
  static bar () {
    this.baz()
  }
  static baz () {
    console.log('hello')
  }
  baz () {
    console.log('world')
  }
}

Foo.bar() // hello

父类的静态方法,可以被子类继承

class Foo {
  static classMethod () {
    return 'hello'
  }
}

class Bar extends Foo {
}

Bar.classMethod() // 'hello'

静态方法也是可以从 super 对象上调用的

class Foo {
  static classMethod () {
    return 'hello'
  }
}

class Bar extends Foo {
  static classMethod () {
    return super.classMethod() + ', too'
  }
}

Bar.classMethod() // "hello, too"

实例属性的两种写法

写法一,写在构造函数中

class IncreasingCounter {
  constructor () {
    this._count = 0
  }
  increment () {
    this._count++
  }
}

写法二,写在类的最顶层

class IncreasingCounter {
  _count = 0
  constructor () {}
  increment () {
    this._count++
  }
}
注意:这种写法属性名前不需要 this、var、let 等关键字

静态属性

静态属性指的是 Class 本身的属性,即 Class.propName,而不是定义在实例对象(this)上的属性

class Foo {
}

Foo.prop = 1
Foo.prop // 1

目前,只有这种写法可行,因为 ES6 明确规定,Class 内部只有静态方法,没有静态属性

私有方法和私有属性

私有方法和私有属性,是只能在类的内部访问的方法和属性,外部不能访问,ES6 不提供私有方法和私有属性,只能通过变通方法模拟实现

一种做法是在命名上加以区别

class Widget {
  // 公有方法
  foo (baz) {
    this._bar(baz)
  }

  // 私有方法
  _bar (baz) {
    return this.snaf = baz
  }
}

上面代码中,_bar 方法前面的下划线,表示这是一个只限于内部使用的私有方法,但是,这种命名不保险,在类的外部,还是可以调用到这个方法

另一种方法就是将私有方法写到 Class 外

class Widget {
  foo (baz) {
    bar.call(this, baz)
  }
}

function bar (baz) {
  return this.snaf = baz
}

上面代码中,foo 是公开方法,内部调用了 bar.call(this, baz),这使得 bar 实际上成为了当前模块的私有方法

还有一种方法是利用 Symbol 值的唯一性,将私有方法的名字命名为一个 Symbol 值

const bar = Symbol('bar')
const snaf = Symbol('snaf')

export default class myClass{
  // 公有方法
  foo (baz) {
    this[bar](baz)
  }
  // 私有方法
  [bar] (baz) {
    return this[snaf] = baz
  }
}

new.target 属性

new 是从构造函数生成实例对象的命令,ES6 为 new 命令引入了一个 new.target 属性,该属性一般用在构造函数之中,返回 new 命令作用于的那个构造函数

如果构造函数不是通过 new 命令或 Reflect.construct() 调用的,new.target 会返回 undefined,因此这个属性可以用来确定构造函数是怎么调用的

下面代码确保构造函数只能通过 new 命令调用

function Person(name) {
  if (new.target !== undefined) {
    this.name = name
  } else {
    throw new Error('必须使用 new 命令生成实例')
  }
}

// 另一种写法
function Person(name) {
  if (new.target === Person) {
    this.name = name
  } else {
    throw new Error('必须使用 new 命令生成实例')
  }
}

let person = new Person('张三') // 正确
let notAPerson = Person.call(person, '张三')  // 报错

Class 内部调用 new.target,返回当前 Class

class Rectangle {
  constructor (length, width) {
    console.log(new.target === Rectangle)
    this.length = length
    this.width = width
  }
}

let obj = new Rectangle(3, 4) // 输出 true

需要注意的是,子类继承父类时,new.target 会返回子类

class Rectangle {
  constructor (length, width) {
    console.log(new.target === Rectangle)
    // ...
  }
}

class Square extends Rectangle {
  constructor (length) {
    super(length, width)
  }
}

let obj = new Square(3) // 输出 false

上面代码中,new.target 会返回子类

利用这个特点,可以写出不能独立使用、必须继承后才能使用的类

class Shape {
  constructor () {
    if (new.target === Shape) {
      throw new Error('本类不能实例化')
    }
  }
}

class Rectangle extends Shape {
  constructor (length, width) {
    super()
    // ...
  }
}

let x = new Shape()  // 报错
let y = new Rectangle(3, 4)  // 正确

上面代码中,Shape类不能被实例化,只能用于继承

super 关键字

super 可以作为方法使用,super 作为函数调用时,代表父类的构造函数

ES6 要求,子类的构造函数必须执行一次 super 函数

class Point {
}

class ColorPoint extends Point {
  constructor (x, y, color) {
    super(x, y) // 调用父类的 constructor(x, y)
    this.color = color
  }
}

在子类的构造函数中,只有调用 super 之后,才可以使用 this 关键字,否则会报错

class Point {
  constructor (x, y) {
    this.x = x
    this.y = y
  }
}

class ColorPoint extends Point {
  constructor (x, y, color) {
    this.color = color // ReferenceError
    super(x, y)
    this.color = color // 正确
  }
}

super 也能作为对象使用,在普通方法中,指向父类的原型对象;在静态方法中,指向父类

class A {
  p () {
    return 2
  }
}

class B extends A {
  constructor () {
    super()
    console.log(super.p()) // 2
  }
}

let b = new B()

上面代码中,子类 console 语句中的 super 在非静态方法之中,所以指向 A.prototype,因此 super.p() 就相当于 A.prototype.p()

class Parent {
  static myMethod(msg) {
    console.log('static', msg);
  }
}

class Child extends Parent {
  static myMethod(msg) {
    super.myMethod(msg)
  }
}

Child.myMethod(1) // static 1

上面代码中,super 在静态方法之中指向父类

在子类的静态方法中通过 super 调用父类的方法时,方法内部的 this 指向当前的子类,而不是子类的实例

class A {
  constructor () {
    this.x = 1
  }
  static print () {
    console.log(this.x)
  }
}

class B extends A {
  constructor () {
    super()
  }
  static m() {
    super.print()
  }
}
B.x = 5
B.m() // 5

上面代码中,静态方法 B.m 里面,super.print 指向父类的静态方法,这个方法里面的 this 指向的是 B,而不是 B 的实例

本文转载自 http://es6.ruanyifeng.com/#docs/class 并进行整理

除特殊说明外本人博客均属原创,转载请注明出处:http://blog.johnhan.cn/blog_1049.html
鄂ICP备17018604号-1  鄂公网安备42060702000030号