JavaScript高级深入浅出:ES6 中的 class

2,645 阅读10分钟

介绍

本文是 JavaScript 高级深入浅出系列的第十一篇,将承接第十篇,了解 ES6 规范中的 class

正文

1. 使用 class 定义类

按照第十篇的方式来定义一个类,不仅和普通函数没有很大区别,而且代码也不容易理解。

  • 在 ES6 规范中,直接使用class关键字来定义类。
  • 但是这只是一种语法糖,最终还是转为传统的方式来定义一个类

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

// 类声明
class Person {

}

// 表达式声明也是可以的,不过不建议在开发中使用
var Student = class {
    
}

2. 类的特性

通过对类的特性的研究,我们会发现,它和构造函数的某些特性是相同的

class Person {}

var p = new Person()

console.log(Person) // [class Person]
console.log(Person.prototype) // {}
console.log(Person.prototype.constructor) // [class Person]
console.log(p.__proto__ === Person.prototype) // true
console.log(typeof Person) // function

2.1 类的构造函数

通过类的构造方法constructor来创建实例时向类传递参数,一个类只能有一个构造函数(因此不能和 Java 一样可以重载构造函数)

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

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

console.log(p) // Person { name: 'alex', age: 18 }

调用构造函数,和之前new调用构造函数的步骤是一样的:

  • 创建一个空对象
  • 将类内部的this指向这个空对象的this
  • 将类的prototype赋值给这个空对象的[[prototype]](__proto__)属性
  • 执行构造函数体
  • 返回这个新创建对象

2.2 类的实例方法

class Person {
  constructor(name, age) {
    this.name = name
    this.age = age
  }
  // running 就是一个实例方法
  running() {
    console.log(`${this.name} is running`)
  }
}

var p = new Person('alex', 18)
p.running()
// 等同于
Person.prototype.running = function() {
    // ......
}

打印Object.getOwnDescripors(Person.prototype),得出

{
  constructor: {
    value: [class Person],
    writable: true,
    enumerable: false,
    configurable: true
  },
  running: {
    value: [Function: running],
    writable: true,
    enumerable: false,
    configurable: true
  }
}

2.3 类的访问器方法

var obj = {
  _name: 'obj',
  get name() {
    return this._name
  },
  set name(val) {
    this._name = val
  },
}

在 ES6 之前,我们可以定义一些构造函数的访问器方法,在class中,可以这样定义:

class Person {
  constructor(name) {
    this._name = name
  }
  // 类的访问器方法
  get name() {
    return this._name
  }
  set name(val) {
    this._name = val
  }
}

const p = new Person('alex')
console.log(Object.getOwnPropertyDescriptors(p))

/*
{
  _name: {
    value: 'alex',
    writable: true,
    enumerable: true,
    configurable: true
  }
}
*/

通过gettersetter访问器函数,可以对读写进行拦截操作

2.4 类的静态方法

静态方法:类独有,实例没有,通过static关键字来描述一个类中的一个方法为静态方法

class Person {
  constructor(name) {
    this._name = name
  }
  static greeting() {
    console.log(`Person 类 say hello`)
  }
}

const p = new Person('alex')
Person.greeting() // 成功
p.greeting() // 报错,没有此方法

比如,我们可以写一个创建Person实例的静态方法

class Person {
  constructor(name) {
    this._name = name
  }
  static greeting() {
    console.log(`Person 类 say hello`)
  }
  static createPerson(...params) {
    return new Person(...params)
  }
}

这样,就可以有两种创建Person实例的方式,区别在于一个是使用new,一个是调用类的静态方法

const p = new Person('alex')
const p2 = Person.createPerson('john')

3. 实现继承

不同于 ES5 的自定义一个工具函数实现继承来讲,ES6 直接写好了一个特性extends来实现继承,不过效果都是一样的

class Person {
  constructor(name) {
    this.name = name
  }
  running() {
    console.log(`${this.name} is running`)
  }
}

// extends 关键字继承父类
class Student extends Person {
  constructor(name, sno) {
    // super 调用父类的构造函数
    // 传入自定义参数
    super(name)
    this.sno = sno
  }
  studying() {
    console.log(`${this.name} is studying, and his sno is ${this.sno}`)
  }
}

const s1 = new Student('alex', 1033)
s1.running() // alex is running
s1.studying() // alex is studying, and his sno is 1033

3.1 super 关键字

我们发现在上面继承一个类时,使用了super,以下为注意事项:

  • 在子(派生)类的构造函数使用this或返回默认对象前,必须通过super调用父类的构造函数
  • super的使用位置:子类的构造函数、实例方法、静态方法

super的用法主要有两种:

// 调用父类的构造方法
super([arguments])
// 调用父类上的静态方法
super.FunctionOnParent([arguments])

4. 方法的重写

// 在实际的开发中,可能会遇到父类的某个方法实现了某些功能
// 子类仍然需要这个方法,但是要加入自己特有的逻辑
// 因此可以使用方法的重写
class Person {
  constructor(name) {
    this.name = name
  }
  running() {
    // ...... 某些逻辑
    console.log(`${this.name} is running`)
  }
}

class Student extends Person {
  constructor(name, sno) {
    super(name)
    this.sno = sno
  }
  // 子类和父类都有同名的方法,就是对于该方法的重写
  running() {
    // 调用父类同名方法,执行逻辑
    super.running()
    console.log(`student ${this.name} is running`)
  }
}

const s1 = new Student('alex', 1033)
s1.running() // 执行了 Person 和 Student 的 running 方法逻辑

5. class 语法糖转为 ES5 代码

由于很多用户的浏览器版本不同,旧版本无法兼容目前新版本的语法,所以为了兼顾大部分用户的体验,一般我们在编写新语法时,会使用一些工具(例如 babel)转为旧语法代码,这样旧版本浏览器就能识别

// ES6 代码
class Person {
  constructor(name) {
    this.name = name
  }
  running() {
    console.log(`${this.name} is running`)
  }
  static staticMethod() {
    console.log(`Person static method`)
  }
}

// babel 转换为 ES5 代码 👇
function _classCallCheck(instance, Constructor) {
  if (!(instance instanceof Constructor)) {
    throw new TypeError('Cannot call a class as a function')
  }
}

function _defineProperties(target, props) {
  for (var i = 0; i < props.length; i++) {
    var descriptor = props[i]
    descriptor.enumerable = descriptor.enumerable || false
    descriptor.configurable = true
    if ('value' in descriptor) descriptor.writable = true
    Object.defineProperty(target, descriptor.key, descriptor)
  }
}

function _createClass(Constructor, protoProps, staticProps) {
  if (protoProps) _defineProperties(Constructor.prototype, protoProps)
  if (staticProps) _defineProperties(Constructor, staticProps)
  return Constructor
}

var Person = /*#__PURE__*/ (function() {
  function Person(name) {
    _classCallCheck(this, Person)

    this.name = name
  }

  _createClass(
    Person,
    [
      {
        key: 'running',
        value: function running() {
          console.log(''.concat(this.name, ' is running'))
        },
      },
    ],
    [
      {
        key: 'staticMethod',
        value: function staticMethod() {
          console.log('Person static method')
        },
      },
    ]
  )

  return Person
})()

5.1 解析代码

首先,将 Person 这个类转为一个函数,内部Person函数先对于调用进行一个校验,如果将Person这个类作为函数进行调用,那么就会报一个TypeError的错误。

var Person = /*#__PURE__*/ (function() {
  function Person(name) {
    _classCallCheck(this, Person)

    this.name = name
  }

  _createClass(
    Person,
    [
      {
        key: 'running',
        value: function running() {
          console.log(''.concat(this.name, ' is running'))
        },
      },
    ],
    [
      {
        key: 'staticMethod',
        value: function staticMethod() {
          console.log('Person static method')
        },
      },
    ]
  )

  return Person
})()

核心代码在于_createClass_defineProperties两个函数

  • _createClass接收三个参数,
    • 第一个参数是需要挂载的目标对象,代码中是Person
    • 第二个参数接收所定义的所有的实例方法的数组,数组中每一项是一个对象,key为方法名称,value为对应的函数
    • 第三个参数接收所定义的所有的静态方法的数组,数组存储的值和第二个参数相同
    • 该函数的作用在于对实例方法和静态方法进行校验,并调用_defineProperties,如果是实例方法,就传入Person.prototype和实例方法数组,如果是静态方法,就传入Person和静态方法数组
  • _defineProperties接收两个参数,
    • 第一个参数是需要挂载属性的目标
    • 第二个参数是需要挂载的属性数组
    • 对于属性数组遍历,通过Object.defineProperty()的方式来一一将属性挂载到目标对象上

简单来说,将用户定义的静态方法挂载在该构造函数中,将用户定义的实例方法挂载在该构造函数的原型上

5.2 /*#__pure__*/

这个是一个标记,意思是标记该函数为一个纯函数,在 webpack 对代码进行压缩优化时,如果遇到了纯函数标记,那么就可以对该函数进行tree-shaking

tree-shaking?如果在分析依赖时,发现该函数没有被用到,那么就会将该函数的所有代码从代码树中删除掉,有效减少了代码的体积

6. ES6 转 ES5 之继承代码解读

// ES6 代码
class Person {}
class Student extends Person {}
// babel 转为 ES5 代码
'use strict'

function _typeof(obj) {
  '@babel/helpers - typeof'
  if (typeof Symbol === 'function' && typeof Symbol.iterator === 'symbol') {
    _typeof = function _typeof(obj) {
      return typeof obj
    }
  } else {
    _typeof = function _typeof(obj) {
      return obj &&
        typeof Symbol === 'function' &&
        obj.constructor === Symbol &&
        obj !== Symbol.prototype
        ? 'symbol'
        : typeof obj
    }
  }
  return _typeof(obj)
}

function _inherits(subClass, superClass) {
  if (typeof superClass !== 'function' && superClass !== null) {
    throw new TypeError('Super expression must either be null or a function')
  }
  subClass.prototype = Object.create(superClass && superClass.prototype, {
    constructor: { value: subClass, writable: true, configurable: true },
  })
  if (superClass) _setPrototypeOf(subClass, superClass)
}

function _setPrototypeOf(o, p) {
  _setPrototypeOf =
    Object.setPrototypeOf ||
    function _setPrototypeOf(o, p) {
      o.__proto__ = p
      return o
    }
  return _setPrototypeOf(o, p)
}

function _createSuper(Derived) {
  var hasNativeReflectConstruct = _isNativeReflectConstruct()
  return function _createSuperInternal() {
    var Super = _getPrototypeOf(Derived),
      result
    if (hasNativeReflectConstruct) {
      var NewTarget = _getPrototypeOf(this).constructor
      result = Reflect.construct(Super, arguments, NewTarget)
    } else {
      result = Super.apply(this, arguments)
    }
    return _possibleConstructorReturn(this, result)
  }
}

function _possibleConstructorReturn(self, call) {
  if (call && (_typeof(call) === 'object' || typeof call === 'function')) {
    return call
  } else if (call !== void 0) {
    throw new TypeError(
      'Derived constructors may only return object or undefined'
    )
  }
  return _assertThisInitialized(self)
}

function _assertThisInitialized(self) {
  if (self === void 0) {
    throw new ReferenceError(
      "this hasn't been initialised - super() hasn't been called"
    )
  }
  return self
}

function _isNativeReflectConstruct() {
  if (typeof Reflect === 'undefined' || !Reflect.construct) return false
  if (Reflect.construct.sham) return false
  if (typeof Proxy === 'function') return true
  try {
    Boolean.prototype.valueOf.call(
      Reflect.construct(Boolean, [], function() {})
    )
    return true
  } catch (e) {
    return false
  }
}

function _getPrototypeOf(o) {
  _getPrototypeOf = Object.setPrototypeOf
    ? Object.getPrototypeOf
    : function _getPrototypeOf(o) {
        return o.__proto__ || Object.getPrototypeOf(o)
      }
  return _getPrototypeOf(o)
}

function _classCallCheck(instance, Constructor) {
  if (!(instance instanceof Constructor)) {
    throw new TypeError('Cannot call a class as a function')
  }
}

var Person = function Person() {
  _classCallCheck(this, Person)
}

var Student = /*#__PURE__*/ (function(_Person) {
  _inherits(Student, _Person)

  var _super = _createSuper(Student)

  function Student() {
    _classCallCheck(this, Student)

    return _super.apply(this, arguments)
  }

  return Student
})(Person)

6.1 解析代码

首先从声明开始

// 声明一个 Person 构造函数,做了边界判断,如果直接调用该构造函数,报错
var Person = function Person() {
  _classCallCheck(this, Person)
}

// 声明子类,这里是实现继承的核心方法
var Student = /*#__PURE__*/ (function(_Person) {
  // _inherits() 函数用于实现继承
  _inherits(Student, _Person)

  var _super = _createSuper(Student)

  function Student() {
    _classCallCheck(this, Student)

    return _super.apply(this, arguments)
  }

  return Student
})(Person)

_inherits函数

function _inherits(subClass, superClass) {
  // 做一下边界判断
  if (typeof superClass !== 'function' && superClass !== null) {
    throw new TypeError('Super expression must either be null or a function')
  }
  // 组合寄生式继承
  subClass.prototype = Object.create(superClass && superClass.prototype, {
    constructor: { value: subClass, writable: true, configurable: true },
  })
  //  _setPrototypeOf 函数,修改 subClass 的原型为 superClass
  // 即 Student.__proto__ = Person
  // 这一步操作目的是静态方法的继承
  if (superClass) _setPrototypeOf(subClass, superClass)
}

function _setPrototypeOf(o, p) {
  // 如果又 Object.setPrototypeOf 方法直接调用该方法
  // 如果没有就手动实现,修改 proto,由于某些历史问题,直接修改 __proto__ 不仅会造成兼容的问题,还会有严重的性能缺陷,因此能直接使用 setPrototypeOf 就不要去修改 __proto__
  _setPrototypeOf =
    Object.setPrototypeOf ||
    function _setPrototypeOf(o, p) {
      o.__proto__ = p
      return o
    }
  return _setPrototypeOf(o, p)
}

_createSuper函数

这一个函数返回一个函数,用于下面的_super.apply(this, arguments)。为什么不要直接调用呢?因为上面做了边界判断,不可以直接调用构造函数,所以包装了一层,该函数返回了createSuperInternal函数(闭包)

// 调用
var _super = _createSuper(Student)
// 函数,Derived 派生
function _createSuper(Derived) {
  // 判断当前环境支不支持 reflect
  var hasNativeReflectConstruct = _isNativeReflectConstruct()
  return function _createSuperInternal() {
    var Super = _getPrototypeOf(Derived),
      result
    if (hasNativeReflectConstruct) {
      var NewTarget = _getPrototypeOf(this).constructor
      result = Reflect.construct(Super, arguments, NewTarget)
    } else {
      result = Super.apply(this, arguments)
    }
    return _possibleConstructorReturn(this, result)
  }
}

createSuperInternal函数

Super获取到原型(上面我们将子类的__proto__ = 父类,所以Super = Person 构造函数)。如果支持 Reflect,就用Reflect.construct来创建一个新对象(ES6 新增的 Reflect)。如果不支持 Reflect,使用Super.apply来创建一个新对象。

最后,调用_possibleConstructorReturn,这个函数还是进行了边界判断,返回的还是创建出来的新对象

function _createSuperInternal() {
    var Super = _getPrototypeOf(Derived),
        result
    if (hasNativeReflectConstruct) {
        var NewTarget = _getPrototypeOf(this).constructor
        result = Reflect.construct(Super, arguments, NewTarget)
    } else {
        result = Super.apply(this, arguments)
    }
    return _possibleConstructorReturn(this, result)
}

回到上文中:

var Student = /*#__PURE__*/ (function(_Person) {
  // _inherits() 函数用于实现继承
  _inherits(Student, _Person)
  // 这里的 _super 返回的就是 `createSuperInternal` 函数
  var _super = _createSuper(Student)

  function Student() {
    _classCallCheck(this, Student)
	// _super.apply 该函数返回了创建出来的新对象
    return _super.apply(this, arguments)
  }

  return Student
})(Person)

7. 继承内置类

我们可以让自己的类继承自内置类,例如Array

class MyArray extends Array {
  constructor(length) {
    super(length)
  }
  set lastValue(value) {
    this[this.length - 1] = value
  }
  get lastValue() {
    return this[this.length - 1]
  }
}

// 可以使用父类 Array 内的方法
const arr = new MyArray(10).fill(10)
// 可以自定义一些功能
arr.lastValue = 9
console.log(arr, arr.lastValue)

8. JS 中的多态

面向对象有三大特性:封装、继承、多态。前两者我们在第10篇中已经详细解释过。下面我们来讨论一下 JS 中的多态

维基百科中对于多态的定义:多态(polymorphism)指为不同数据类型的实体提供统一的接口,或使用一个单一的符号来表示多个不同的类型

简单理解来说:不同的数据类型进行同一个操作,表现出不同的行为,这就是多态的表现

从定义来看,JS 是存在于多态的

8.1 传统的面向对象语言中的多态

在传统的面向对象的语言中(如 Java),多态一般表现为重写重载

  • 重写,一般是子类重写父类某个方法的实现过程,要求方法名、参数类型、参数个数、返回值类型都相同
  • 重载,同一个方法可以有不同的实现细节,方法名相同,参数类型、参数个数不同(重载究竟属不属于多态大多数的观点说的语焉不详,网上部分人认为重载仅属于函数的多态,并不属于面向对象中的多态,这里由你来自我辨别)

ECMAScript 并没有规范方法的重写与重载,甚至在 JS 中无法实现重载,但是可以实现重写

8.2 JS 中的多态

严格意义上的多态,是发生在类内部的,但是 JS 由于其是动态语言,所以非常灵活,因此也会有另一种多态的表现形式:

举个例子来理解一下吧:

var baiduMap = {
  render: function () {
    console.log('渲染百度地图')
  },
}

var googleMap = {
  render: function () {
    console.log('渲染谷歌地图')
  },
}

两个地图的行为是相同的(都拥有render行为,区别在于render做出的操作),按照这种形式来写代码,很容易就会发现,多个地图需要写多个对象,但是对象的行为都是相同的,会造成代码大量冗余。这个时候,我们可以将render抽离出来,甚至于不同的两个类型也可以实现多态的形式

// 提供统一的接口
var map = {
  render: function (msg) {
    console.log(msg)
  },
}

var googleMap = {
  msg: '渲染谷歌地图',
}

class BaiduMap {
  constructor(msg) {
    this.msg = msg
  }
}
const baiduMap = new BaiduMap('渲染百度地图')

// 不同的类型触发统一的接口会有不同的表现
map.render(googleMap.msg) // 渲染谷歌地图
map.render(baiduMap.msg) // 渲染百度地图

这样的代码,就是一个多态的表现。多态是一种编程思想,不需要拘泥于某一种表现形式,只要满足于维基百科中的定义就是多态的表现

总结

本文中,你学到了:

  • 如果使用class定义一个类
  • 类的实例方法、静态方法、属性
  • 如何使用extends来实现继承
  • class通过babel转为 ES5 代码的表现
  • JS 中的多态的表现