ECMAScript6-Class

108 阅读7分钟

ES6中引入JavaScript类实质上是JavaScript现有的基于原型的继承的语法糖。类语法不会为JavaScript引入新的面向对象的继承模型。本质和ES5一样,各种静态属性和方法、实例属性和原型方法、属性绑定位置一样。

class Person {
  // constructor 相当于ES5的构造函数,所有实例属性都要绑定在这里:实例对象 this
  constructor(name) {
    this.name = name
  }
  // 方法绑定在原型对象上,借助原型链继承,节省内存空间
  hello() {
    console.log('Hello, my name is ' + this.name + '.')
  }
  // 给类绑定静态属性和方法
  static count = 0
  static log() {}
}

let p = new Person('lxx')
p.hello()

类的构造方法

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

class Point {}

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

上面代码定义了一个空的类 Point,JavaScript 引擎会自动为其添加一个空的 constructor方法。 constructor 方法默认返回实例对象(this),但完全可以指定返回另一个对象。

class Foo {
  constructor() {
    return Object.create(null)
  }
}

上面代码,constructor 方法返回一个全新的对象,结果导致实例对象不是 Foo 类的实例。

类的原型方法

class Person {
  constructor(name,age) {...}
  // 类的原型方法
  running() {
    console.log(this.name + '正在跑步~')
  }
  eating() {
    console.log(this.name + '正在吃饭~')
  }
}

类的访问器方法

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

class MyClass {
  get prop() {
    return 'getter'
  }
  set prop(val) {
    console.log('setter:' + val)
  }
}

let inst = new MyClass()
inst.prop // 'getter'
inst.prop = 'hello' // 'setter:hello'

存值函数和取值函数设置在属性的属性描述对象上,这与ES5完全一致。

class CustomHTMLElement {
  constructor(element) {
    this.element = element
  }

  get html() {
    return this.element.innerHTML
  }

  set html(value) {
    this.element.innerHTML = value
  }
}

var descriptor = Object.getOwnPropertyDescriptor(
  CustomHTMLElement.prototype, "html"
)

"get" in descriptor  // true
"set" in descriptor  // true

类的静态方法

如果在一个方法前加上 static 关键字,就表示该方法不会被实例继承,而是通过类直接调用,这就成为静态方法。由于静态方法是在构造函数而非实例调用的,所以在静态方法中使用 this 没什么意义。

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

上面代码中,静态方法 bar 中执行了 this.baz,这里的 this 指的是 Foo 类,而不是 Foo 的实例。另外,从这个例子还可以看出,静态方法可以与非静态方法重名。

私有方法和私有属性

现有的解决方案

私有方法和私有属性,是只能在类的内部访问的方法和属性,外部不能访问。这是常见需求,有利于代码的封装。但ES6不提供,只能通过变通方法模拟实现。

  1. 命名上区别
class Person {
  constructor(name) {
    this.name = name
  }
  sayName() {
    console.log(this.name)
  }
  _sayBye() {
    console.log('bye~bye~')
  }
}

let p = new Person('lxx')
p._sayBye()

上面代码中,_sayBye 方法前面的下划线,视觉上表示这是一个只限于内部使用的私有方法。但在类的外部,还是可以调用这个方法。 2. 私有方法移出类,因为类内部的所有方法都是对外可见的

class Person{
  constructor(name) {
    this.name = name
  }
  sayName() {
    _sayBye.call(this,this.name)
  }
}

function _sayBye(name) {
  console.log(name)
}
let p = new Person('lxx')
p.sayName() // lxx

上面代码中,sayName 是公开方法,内部调用了 _sayBye.call(this,this.name)。这使得 _sayBye 实际上成为了当前类的私有方法。

  1. 每一个从 Symbol() 函数返回的 symbol 值都是唯一的,利用 Symbol 值的唯一性,将私有方法命名为一个 Symbol 值。
 const MyObject = (function() {
   let _value = Symbol('value')

   class MyObject {
     constructor() {
       this[_value] = 123
     }

     getValue() {
       return this[_value]
     }

     setValue(newValue) {
       this[_value] = newValue
     }
   }

   return MyObject
 })()
 
 const o = new MyObject()
 o[Symbol('value')] // undefined
 o.getValue() // 123

Class 表达式定义方式

与函数一样,类也可以使用表达式的形式定义。

const MyClass = class Me {
  getClassName() {
    return Me.name
  }
}

需要注意的是这个类的名字是 Me,但是 Me 只在 Class 的内部可用,指代当前类。在 Class 外部,这个类只能用 MyClass 引用。

let inst = new MyClass()
inst.getClassName() // Me
Me.name // ReferenceError: Me is not defined

类的立即实例化

let p = new class {
  constructor(name) {
    this.name = name
  }
  
  sayName() {
    console.log(this.name)
  }
}('lxx')

p.sayName() // lxx

Class 的继承

子类既可以继承父类中的原型方法、静态方法,也可以重写父类的原型方法和静态方法。甚至可以通过 super.method() 的方式在子类方法中先调用父类方法。

class Person {
  constructor(name, age, height) {
    this.name = name
    this.age = age
    this.height = height
  }
  running() {
    console.log(this.name + '正在跑步~')
  }
  eating() {
    console.log(this.name + '正在吃饭~')
  }
  personMethod() {
    console.log('处理逻辑1')
    console.log('处理逻辑2')
    console.log('处理逻辑3')
  }
  static staticMethod() {
    console.log('父类的静态方法')
  }
}

class Student extends Person {
  constructor(name, age, sno) {
    super(name, age)
    this.sno = sno
  }
  // 子类中定义与父类的同名方法
  running() {
    console.log('我是子类中的running方法')
  }
  studentMethod() {
    // 调用父类的方法
    super.personMethod()
    console.log('处理逻辑4')
  }
}

let s = new Student('xxx', 24, 1.88)
// 子类方法覆写父类方法
s.running()

// 继承父类的原型方法
s.eating()
s.personMethod()

// 继承父类的静态方法
Student.staticMethod()

// 同时调用父类和自己的方法
s.studentMethod()

super

super 既可以当做函数使用,也可以当做对象使用。这两种情况下,用法完全不同。

super在构造器中

super 作为函数调用,代表父类构造函数。
在继承中,子类的构造函数在使用 this 之前必须执行一次 super 函数。这是必须的,否则引擎会报错。

class Person {
  constructor(name, age, height) {
    this.name = name
    this.age = age
    this.height = height
  }
}

class Student extends Person {
  constructor(name, age, sno) {
    super(name, age) // 将 name age 交给父类处理
    this.sno = sno
  }
}

var s1 = new Student('lxx', 25, 101)

💣 super 虽然代表了父类构造函数,但返回的是子类 Student 的实例,即 super 内部的 this 代表 Student 的实例,因此 super() 在此相当于 Person.call(this) 暨相当于 ES5 盗用父构造函数。 子类必须在 constructor 方法中调用 super 方法,用在其他地方会报错。因为子类自己的 this 对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法。然后再对其进行加工,加上子类自己的实例属性和方法。如果不调用 super 方法,子类就得不到 this 对象。

class Point {...}
class ColorPoint extends Point {
  constructor() {}
}

let cp = new ColorPoint() // ReferenceError

上面代码中,ColorPoint 继承了父类 Point,但它的构造函数没有调用 super 方法,导致新建实例时报错。

原型链分析

// Student 的原型方法在自己的原型对象中
s1.__proto__
// Student.prototype 指向 Person.prototype
s1.__proto__.__proto__

相当于使用了原型继承函数,以 Person.prototype 为原型创建了一个新对象,让新对象作为 StudentStudent.prototype 实现了继承。

super在方法中

super 在原型方法中使用,指向父类的原型对象;在静态方法中,指向父类。

class Person {
  constructor(name, age, height) {
    this.name = name
    this.age = age
    this.height = height
  }
  personMethod() {
    console.log('处理逻辑1')
  }
}

class Student extends Person {
  constructor(name, age, sno) {
    super(name, age)
    this.sno = sno
  }
  
  studentMethod() {
    // 通过super调用父类方法
    super.personMethod()
    console.log('处理逻辑2')
  }
}

子类 Student 当中的 super.personMethod(),就是将 super 当做一个对象使用。这时 super 在普通方法中指向 Person.prototype,所以 super.personMethod() 就相当于 Person.prototype.personMethod()

class A {
  constructor() {
    this.p = 2 // 父类的实例属性
  }
}

class B extends A {
  get m() {
    return super.p
  }
}
let b = new B()
b.m // undefined

💣 由于 super 指向父类的原型对象,所以定义在父类实例上的方法或属性,是无法通过 super 调用的。实例属性和实例方法是绑定在使用构造函数创建出来的实例对象上的。 ES6规定在子类普通方法中通过 super 调用父类的方法时,父类方法内部的 this 指向子类实例。

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

class B extends A {
  constructor() {
    super()
    this.x = 2
  }
  m() {
    super.print() // 通过 super 调用父类方法
  }
}

let b = new B()
b.m() // 2

更多super及this指向问题,请查看《ES6入门》

继承父类静态方法

父类的静态方法,也会被子类继承。

class A {
  static hello() {
   console.log('hello world')
  }
}
class B extends A {}
B.hello() // hello world

继承父类属性

let obj = { name:'lxx' }
let obj2 = {
  __proto__:obj,
  name:'xxx',
  getName() {
    return super.name
  }
}
console.log(obj2.getName())

与ES5的对比

function Person 是“提升的”而class Person并不是。在实例化一个 class 之前必须先声明它。全局作用域中的 class Person 创建了这个作用域的一个词法标识符 Person,但是和 function Person 不一样,并没有创建一个同名的全局对象属性。

function Person(name, age) {
  this.name = name
  this.age = age
}

// 静态属性绑定在类(构造函数)上
Person.sex = 'man'

// 实例方法绑定在实例的原型对象上
Person.prototype.run = function () {
  console.log(this.name + 'run!')
}