knzn.net

11 ES6 中类的使用

我们会发现,按照前面的构造函数形式创建 类,不仅仅和编写普通的函数过于相似,而且代码并不容易理解。

在 ES6(ECMAScript2015)新的标准中使用了 class 关键字来直接定义类;但是类本质上依然是前面所讲的构造函数、原型链的语法糖而已;所以学好了前面的构造函数、原型链更有利于我们理解类的概念和继承关系;

认识 class 定义类

那么,如何使用 class 来定义一个类呢?可以使用两种方式来声明类:类声明和类表达式;

// 类的声明
class Person {}

// 类的表达式
var Animal = class {}

类和构造函数的异同

我们来研究一下类的一些特性:你会发现它和我们的构造函数的特性其实是一致的;

var p = new Person()
console.log(p.__proto__ === Person.prototype) // true
console.log(Person.prototype) // { constructor: class Person, __proto__: Object.prototype }
console.log(Person.prototype.__proto__) // Object.prototype
console.log(Person.prototype.constructor) // class Person
console.log(typeof Person) // function

类的构造函数

如果我们希望在创建对象的时候给类传递一些参数,这个时候应该如何做呢?

每个类都可以有一个自己的构造函数(方法),这个方法的名称是固定的 constructor;当我们通过 new 操作符,操作一个类的时候会调用这个类的构造函数 constructor;每个类只能有一个构造函数,如果包含多个构造函数,那么会抛出异常;

当我们通过 new 关键字操作类的时候,会调用这个 constructor 函数,并且执行如下操作:

  • 1.在内存中创建一个新的对象(空对象);
  • 2.这个对象内部的[[prototype]]属性会被赋值为该类的 prototype 属性;
  • 3.构造函数内部的 this,会指向创建出来的新对象;
  • 4.执行构造函数的内部代码(函数体代码);
  • 5.如果构造函数没有返回非空对象,则返回创建出来的新对象;
// 类的声明
class Person {
  // 类的构造方法
  // 注意: 一个类只能有一个构造函数
  // 1.在内存中创建一个对象 moni = {}
  // 2.将类的原型prototype赋值给创建出来的对象 moni.__proto__ = Person.prototype
  // 3.将对象赋值给函数的this: new绑定 this = moni
  // 4.执行函数体中的代码
  // 5.自动返回创建出来的对象
  constructor(name, age) {
    this.name = name
    this.age = age
  }
}

var p1 = new Person('why', 18)
var p2 = new Person('kobe', 30)
console.log(p1, p2)

类的实例方法

在上面我们定义的属性都是直接放到了 this 上,也就意味着它是放到了创建出来的新对象中:

在前面我们说过对于实例的方法,我们是希望放到原型上的,这样可以被多个实例来共享;这个时候我们可以直接在类中定义;

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

  // 普通的实例方法
  // 创建出来的对象进行访问
  // var p = new Person()
  // p.eating()
  eating() {
    console.log(this.name + ' eating~')
  }

  running() {
    console.log(this.name + ' running~')
  }
}

var p = new Person('why', 18)
p.eating()
p.running()

类的访问器方法

我们之前讲对象的属性描述符时有讲过对象可以添加 setter 和 getter 函数的,那么类也是可以的:

class Person {
  constructor() {
    this._address = '广州市'
  }

  // 类的访问器方法
  get address() {
    console.log('拦截访问操作')
    return this._address
  }

  set address(newAddress) {
    console.log('拦截设置操作')
    this._address = newAddress
  }
}

var p = new Person('why', 18)

console.log(p.address)
p.address = '北京市'
console.log(p.address)

类的静态方法

静态方法通常用于定义直接使用类来执行的方法,不需要有类的实例,使用 static 关键字来定义:

var names = ['abc', 'cba', 'nba']

class Person {
  // 类的静态方法(类方法)
  static randomPerson() {
    var nameIndex = Math.floor(Math.random() * names.length)
    var name = names[nameIndex]
    var age = Math.floor(Math.random() * 100)
    return new Person(name, age)
  }
}

for (var i = 0; i < 50; i++) {
  console.log(Person.randomPerson())
}

ES6 类的继承 - extends

前面我们花了很大的篇幅讨论了在 ES5 中实现继承的方案,虽然最终实现了相对满意的继承机制,但是过程却依然 是非常繁琐的。

在 ES6 中新增了使用 extends 关键字,可以方便的帮助我们实现继承:

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

  running() {
    console.log(this.name + ' running~')
  }

  eating() {
    console.log(this.name + ' eating~')
  }
}

// Student称之为子类(派生类)
class Student extends Person {
  // JS引擎在解析子类的时候就有要求, 如果我们有实现继承
  // 那么子类的构造方法中, 在使用this之前
  constructor(name, age, sno) {
    super(name, age)
    this.sno = sno
  }
}

super 关键字

我们会发现在上面的代码中我使用了一个 super 关键字,这个 super 关键字有不同的使用方式:

注意:在子(派生)类的构造函数中使用 this 或者返回默认对象之前,必须先通过 super 调用父类的构造函数!

super 的使用位置有三个:子类的构造函数、实例方法、静态方法;

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

  running() {
    console.log(this.name + ' running~')
  }

  eating() {
    console.log(this.name + ' eating~')
  }

  personMethod() {
    console.log('处理逻辑1')
    console.log('处理逻辑2')
    console.log('处理逻辑3')
  }

  static staticMethod() {
    console.log('PersonStaticMethod')
  }
}

// Student称之为子类(派生类)
class Student extends Person {
  // JS引擎在解析子类的时候就有要求, 如果我们有实现继承
  // 那么子类的构造方法中, 在使用this之前
  constructor(name, age, sno) {
    super(name, age) // super 代表父类的构造函数
    this.sno = sno
  }

  studying() {
    console.log(this.name + ' studying~')
  }

  // 类对父类的方法的重写
  running() {
    console.log('student ' + this.name + ' running')
  }

  // 重写personMethod方法
  personMethod() {
    // 复用父类中的处理逻辑
    super.personMethod() // super 代表 父类的prototype

    console.log('处理逻辑4')
    console.log('处理逻辑5')
    console.log('处理逻辑6')
  }

  // 重写静态方法
  static staticMethod() {
    super.staticMethod() // super 代表 父类
    console.log('StudentStaticMethod')
  }
}

var stu = new Student('why', 18, 111)
console.log(stu)

// console.log(Object.getOwnPropertyDescriptors(stu.__proto__))
// console.log(Object.getOwnPropertyDescriptors(stu.__proto__.__proto__))

stu.eating()
stu.running()

stu.personMethod()

Student.staticMethod()

console.log(Object.getOwnPropertyDescriptors(Person))

继承内置类

我们也可以让我们的类继承自内置类,比如 Array:

class HYArray extends Array {
  firstItem() {
    return this[0]
  }

  lastItem() {
    return this[this.length - 1]
  }
}

var arr = new HYArray(1, 2, 3)
console.log(arr.firstItem())
console.log(arr.lastItem())

类的混入 mixin

JavaScript 的类只支持单继承:也就是只能有一个父类

那么在开发中我们我们需要在一个类中添加更多相似的功能时,应该如何来做呢? 这个时候我们可以使用混入(mixin);

class Person {}

function mixinRunner(BaseClass) {
  class NewClass extends BaseClass {
    running() {
      console.log('running~')
    }
  }
  return NewClass
}

function mixinEater(BaseClass) {
  return class extends BaseClass {
    eating() {
      console.log('eating~')
    }
  }
}

// 在JS中类只能有一个父类: 单继承
class Student extends Person {}

var NewStudent = mixinEater(mixinRunner(Student))
var ns = new NewStudent()
ns.running()
ns.eating()

JavaScript 中的多态

面向对象的三大特性:封装、继承、多态。前面两个我们都已经详细解析过了,接下来我们讨论一下 JavaScript 的多态。

JavaScript 有多态吗?

不同的数据类型进行同一个操作,表现出不同的行为,就是多态的体现。

那么从上面的定义来看,JavaScript 是一定存在多态的

function sum(m, n) {
  return m + n
}

sum(20, 30)
sum('abc', 'cba')
// 多态: 当对不同的数据类型执行同一个操作时, 如果表现出来的行为(形态)不一样, 那么就是多态的体现.
function calcArea(foo) {
  console.log(foo.getArea())
}

var obj1 = {
  name: 'why',
  getArea: function () {
    return 1000
  },
}

class Person {
  getArea() {
    return 100
  }
}

var p = new Person()

calcArea(obj1)
calcArea(p)

目录