Skip to content

大多数面向对象的编程语言都支持类和类继承的特性,而 JavaScript 却不支持这些特性,只能通过其他方法定义并关联多个相似的对象。

Es5 中,我们会利用原型来实现类似的类特性。

而在 Es6 中,正式定义了 JSclass 语法。

9-1.ES5中的类

Es5 中,我们可以通过构造函数和原型来模拟实现类的语法:

js
// 构造函数
function Factory (name) {
  // 实例属性
  this.name = name
}
// 原型方法
Factory.prototype.getName = function() {
  return this.name
}
// 静态属性
Factory.time = new Date('1994-10-19')
// 静态方法
Factory.getTime = function() {
  return this.time
}
// 定义实例
var p1 = new Factory('Tom')
var p2 = new Factory('Jerry')
console.log(p1.getName())
console.log(p2.getName())
// 调用静态方法
Factory.getTime()

9-2.Es6中的类

Es6 中新增了 class 语法。

Es6 中的 class 只是构造函数的一种语法糖。

在上一节中的构造函数,转化为 class 写法的话,如下:

js
class Factory {
  constructor(name) {
    // 实例属性
    this.name = name
  }
  // 原型方法
  getName() {
    return this.name
  }
  // 静态属性
  static time = 'time'
  // 静态方法
  static getTime() {
    return this.time
  }
}

9-3.类的声明与表达式

其实,Es6 中的类本身是一种语法糖。它的实现依然借助于构造函数

那么,如同构造函数有声明表达式之分,类也能够使用声明或者表达式形式。

9-3-1.类的声明

js
class Factory {
  constructor (name) {
    // 实例属性
    this.name = name
  }
  // 原型方法
  getName () {
    return this.name
  }
}
// class其实是function
console.log(typeof Factory) // function

constructor 指代的就是构造函数。它内部通常用来定义实例属性

另外在 constructor 内部是可以接收到 arguments 的。

类的声明形式有一下特点:

  1. 不会存在声明作用域提升。这一点要与函数声明区分开来,因为函数声明在 JS 中会提升到文档顶部。
  2. 内部代码执行严格模式。即代码中存在 use strict
  3. 原型上的属性和方法,不可枚举。
  4. 每个类都有一个 [[Construct]] 的内部方法。通过关键字 new 调用那些不含 [[construct]] 的方法会导致程序抛出错误。
  5. 类只能使用 new 进行调用,否则会抛出错误。因为内部已经利用 new.target 锁定。
  6. 在类中修改类名会导致程序报错。在外部可以修改类名。

根据上述特点,我们借助 Es5 来实现下 class:

js
let Factory = (function () {
  'use strict'
  const Factory = function (name) {
    if (new.target === undefined) {
      throw new Error('Factory must be called by new')
    }
    this.name = name
  }
  Object.defineProperty(Factory.prototype, 'getName', {
    value: function (name) {
      return this.name
    },
    writable: true,
    configable: true,
    enumerable: false
  })
  return Factory 
})()

9-3-2.类的表达式

类的表达式形式与函数的表达式形式很类似。

js
// 普通类表达式
let Factory = class {
  constructor (name) {
    this.name = name
  }
  getName () {
    return this.name
  }
}
console.log(typeof Factory) // function
js
// 命名类表达式
let Factory = class Factory2 {
  constructor (name) {
    this.name = name
  }
  getName () {
    return this.name
  }
}
console.log(typeof Factory) // function
console.log(typeof Factory2) // undefined

9-4.原型属性

我们通常在 constructor 里定义实例属性

在原型上定义原型属性

js
class Factory {
  constructor (name) {
    this.name = name
  }
  getName () {
    return this.name
  }
}

此外,原型上也可以定义访问器属性

js
class Factory {
  constructor (name) {
    this.name = name
  }
  getName () {
    return this.name
  }
  get alias () {
    return 'alias name'
  }
  set alias (value) {
    this.name = value
  }
}
var f = new Factory('Tom')
console.log(f.getName()) // Tom
console.log(f.alias) // alias name
f.alias = 'Jerry'
console.log(f.name) // Jerry
console.log(f.getName()) // Jerry
console.log(f.alias) // alias name

9-5.静态属性

静态属性可以用来挂载构造函数的属性/方法

Es5 中实现静态属性的方式是:

js
function Factory () {
}
Factory.id = '1019'
Factory.getId = function () {
  console.log(this.id)
}
console.log(Factory.getId()) // '1019'

而在 Es6class 中提供了 static 关键字,能够更方便的定义静态属性:

js
class Factory {
  constructor () {
  }
  static id = '1019'
  static getId () {
    return this.id
  }
}
console.log(Factory.getId()) // '1019'

9-6.私有属性

我们一般声明私有属性时通常会以 _ 符号为前缀,但这种形式其实只是一种隐性约定。并不能完全阻止外部对于私有属性的访问。

class 中提供了 # 符号。

# 为前缀的属性视为私有属性。外部无法访问。

js
class Factory {
  // 私有属性其实也是挂载原型prototype上
  #id = '1019'
  #getId () {
    return this.#id
  }
  getId () {
    return this.#getId()
  }
}
var f = new Factory()
console.log(f.getId()) // '1019'
console(f.#id) // Uncaught SyntaxError: Private field '#id' must be declared in an enclosing class

9-7.类的继承

关于 Js 的继承,这里有一章专门的详细介绍

本章大致记录下重点内容。

9-7-1.extends

Es5 中,实现继承最理想的方式是采用寄生组合式:

js
// Factory
function Factory (name) {
  this.name = name
}
Factory.prototype.getName = function () {
  return this.name
}
// People
function People (name) {
  Factory.call(this, name)
}
People.prototype = Object.create(Factory.prototype, {
  constructor: {
    value: People,
    writable: true,
    enumerable: true,
    configable: true
  }
})
var p = new People('Tom')
console.log(p.name) // Tom
console.log(p.getName()) // Tom
console.log(p.__proto__.constructor) // People

Es6 中提供了,更加优雅的方式,利用 extends 关键字:

js
// Factory
class Factory {
  constructor (name) {
    this.name = name
  }
  getName () {
    return this.name
  }
}
// People
class People extends Factory {
  constructor (name) {
    // 这里必须调用super 用以继承父类的实例属性和静态属性
    super(name)
  }
}
var p = new People('Tom')
console.log(p.name) // Tom
console.log(p.getName()) // Tom
console.log(p.__proto__.constructor) // People

TIP

如果在 extends 中不调用 super 的话,会报错:

Uncaught ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor

9-7-2.super

super 关键字主要用于 class 的继承中。而 class 的继承依赖于 extends 关键字。

super 既能作为函数调用,又能作为对象使用。

super 的指向要根据实际情况判断,其内部绑定的 this 也是需要根据实际情况判断。

根据使用场景的不同,super 的指向也不同

js
// class的继承,核心就在于 super 这个关键字
class SuperClass {
  constructor(name = 'super-name') {
    this.name = name
  }
  getName() {
    return this.name
  }
  static time = 'super-time'
  static getTime() {
    return this.time
  }
}

class SubClass extends SuperClass{
  constructor(name = 'sub-name', age = 19, job = 'programmer') {
    // ① super指向父类构造函数
    super(name)
    this.age = age
    // ② super指向子类实例
    super.job = job
  }
  getSubName() {
    // ③ super指向父类原型,this会绑定当前子类实例 而不是父类实例
    return super.getName() // 应该是 'sub-name' 而不是 'super-name'
  }
  static time = 'sub-time'
  static getSubTime() {
    // ④ super指向父类构造函数,this会绑定当前子类 而不是父类
    return super.getTime()
  }
}

const sub = new SubClass()
console.log(sub.job) // programmer
console.log(sub.getSubName()) // 'sub-name'
console.log(SubClass.getSubTime()) // 'sub-time'

// 子类的构造函数必须执行一次super函数
// super绑定子类的this
// Object.getPrototypeOf() 方法返回指定对象的原型(内部[[Prototype]]属性的值)