JS 创建实例对象的方法是通过构造函数的方式,与传统编程语言差异很大,不容易理解,所以引入了 ClassES6
class 是 构造函数原型 的语法糖

语法

class MyClass {
  constructor(name) {
    this.name = name
  }
  sayHello() {
    console.log('Hello World', this.name)
  }
}
 
let obj = new MyClass('name')
obj.sayHello()

class 是一个函数:typeof MyClass // 'function'

以上代码等同于:

// constructor 相当于 构造函数
function MyClass(name) {
  this.name = name
}
 
// 类方法 相当于 原型上的方法
MyClass.prototype.sayHello = function() {
  console.log('Hello World')
}
 
MyClass.prototype.constructor = MyClass

constructor 中的代码相当于函数内的代码(即对象实例上的属性),其他方法相当于原型上的
constructor 函数如果返回了对象,则这个对象就是生成的类实例对象 (类似 构造函数
class 里也有 new.target

但是还是有一些不同:

  1. 通过 class 创建的函数具有特殊的内部属性标记 [[IsClassConstructor]]: true,所以只能通过 new 调用
  2. 类方法不可枚举
  3. 类中自动开启严格模式

特性

有函数的特性:类可以作为变量,也有类表达式
也有对象的特性:可以设置 getter/setter,可以使用计算属性

类字段ES2022

实例属性现在除了可以定义在 constructor() 方法里面的 this 上面,也可以定义在最外层
类字段在每个独立对象中设置,而不是像类方法那样在 MyClass.prototype 上

class MyClass {
  name = 'js' // 类字段
  
  sayHello() {
    console.log(this.name)
  }
}
 
// 相当于传统构造函数中的属性初始化
function MyClass() {
  this.name = 'js'
}

静态属性ES2022

在类字段前面加上 static 关键字
作用:属于整个类,但不属于某个具体对象的属性

class MyClass {
  static name = 'class'
}
  • 通过类名来调用:MyClass.name
  • 可以被继承(但是是浅拷贝继承)

静态方法ES2022

在函数前面加上 static 关键字
作用:属于整个类,但不属于某个具体对象的方法

class MyClass {
  static f() {
    alert(this === MyClass)
  }
}
  • 通过类名来调用:MyClass.f()
  • 静态方法的 this 指的是类,而不是实例
  • 静态方法可与非静态方法重名
  • 可以被继承

静态块ES2022

允许在类的内部设置一个代码块,在类生成时运行且只运行一次,主要作用是对静态属性进行初始化

受保护属性和方法

通常以下划线 _ 作为前缀,这是约定,并没有在语法层面限制
然后使用 get/set 来访问

私有属性和方法ES2022

  • # 作为前缀,这是语法层面的限制
  • 只能在类的内部被访问,外部访问会报错
  • 前面可以加 static,表示静态私有属性或方法
  • 不能通过中括号的形式 this[propName] 访问
  • this 指向和公共方法一样
  • 不能被继承

相当于传统的闭包:

function Person(age) {
  // 私有变量
  let _age = age
 
  // 公有方法
  this.getAge = function() {
    return _age;
  }
 
  this.setAge = function(newAge) {
    if (newAge > 0) {
      _age = newAge
    }
  }
}

this

类的方法内部如果含有 this,它默认指向类的实例。
但是,如果将这个方法提取出来单独使用,根据 this 绑定规则,所以 this 实际指向的是 undefined

class myClass {
  f() {
    console.log('this:', this)
  }
}
 
const myclass = new myClass()
const { f } = myclass
f() // this: undefined

解决方法:
1.在构造方法中绑定 this

class myClass {
  constructor() {
    this.f = this.f.bind(this)
  }
 
  f() {
    console.log('this:', this)
  }
}

2.箭头函数:

class myClass {
  f = () => {
    console.log(this)
  }
}

继承

一个类扩展另一个类的一种方式,可以在现有功能之上创建新功能,extends 后面可以跟构造函数(包括类)或 null 的表达式

class SubClass extends MyClass {
  f() {
    console.log(this.name)
  }
}

extends

  • extends 将 子类 的 prototype 的 __proto__ 指向 父类 的 prototype,实现了方法的继承,即:SubClass.prototype.__proto__ = MyClass.prototype
  • 同时将 子类 的 __proto__ 指向 父类,实现了静态属性和方法的继承,即:SubClass.__proto__ = MyClass

500

在 extends 后面可以接任意表达式,如一个函数调用的结果作为父类

constructor

如果子类没有 constructor,则会默认生成下面的(调用父类的构造函数并传递所有参数)

class SubClass extends MyClass {
  // 自动生成的
  constructor(...args) {
    super(...args)
  }
}

但是如果写了自己的 constructor,则必须在 constructor 里调用 super(…),并且一定要在使用 this 之前调用 为什么?

继承类(derived constructor)的构造函数与其他函数之间有区别,它具有特殊内部属性 [[ConstructorKind]]:"derived" ,影响了它的 new 行为:

  • 当通过 new 执行一个常规函数时,它将创建一个空对象,并将这个空对象赋值给 this
  • 但是当继承的 constructor 执行时,它不会执行此操作。它期望父类的 constructor 来完成这项工作。因为子类自己的 this 对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,添加子类自己的实例属性和方法。如果不调用 super() 方法,子类就得不到自己的 this 对象。所以在执行 super 之前,this 指向的对象还没有创建,会报错

重写方法

子类可以重写父类的同名方法

super

子类可以重写父类的方法,但有时候又不希望完全重写,而是在父类的方法上面进行扩展
super 用法:

  • 作为对象调用(在静态方法中,super 表示父类。在其他方法中,super 表示父类的原型对象):super.method(...)super.prop
    • 在子类普通方法中通过 super 调用父类的方法时,父类方法内部的 this 指向当前的子类实例
    • 在子类的静态方法中通过 super 调用父类的方法时,父类方法内部的 this 指向当前的子类,而不是子类的实例
    • 上面是读操作的规则。如果对 super 进行写操作(如 super.x = 1),则 super 表示子类实例
  • 作为函数调用(代表父类的构造函数)
    • super(...):调用一个父类 constructor(只能在子类的 constructor 中)

Warning

箭头函数没有 super

重写类字段

子类中可以重写父类的类字段,但是父类在其 constructor 中访问同名的类字段时还是会使用父类自己的类字段,而不是子类的类字段 因为类字段是这样初始化的:

  • 对于基类,在构造函数调用前初始化
  • 对于派生类,在 super() 之后立刻初始化 在父类构造器被执行的时候,子类还没有自己的类字段

NOTE

这种行为仅在一个被重写的字段被父类构造器使用时才会显现出来(这种字段与方法之间微妙的区别只特定于 JavaScript)
可以通过使用方法或者 getter/setter 替代类字段,来修复这个问题

HomeObject

super 如何获取到父类原型的方法并使用自己的 this?super 的原理是?

简单尝试:
使用 this.__proto__.method() 获取父 method(为了简化使用对象)

let animal = {
  name: "Animal",
  eat() {
    alert(`${this.name} eats.`)
  }
}
 
let rabbit = {
  __proto__: animal,
  name: "Rabbit",
  eat() {
    // 这就是 super.eat() 可以大概工作的方式
    this.__proto__.eat.call(this)
  }
}
 
rabbit.eat() // Rabbit eats.

貌似可行,继续多次继承:

let animal = {
  name: "Animal",
  eat() {
    alert(`${this.name} eats.`)
  }
}
 
let rabbit = {
  __proto__: animal,
  eat() {
    this.__proto__.eat.call(this) // (2) 根据显式绑定,this 是 longEar,相当于 rabbit.eat.call(longEar)
  }
}
 
let longEar = {
  __proto__: rabbit,
  eat() {
    this.__proto__.eat.call(this) // (1) 根据隐式绑定,this 是 longEar
  }
}
 
longEar.eat() // Error: Maximum call stack size exceeded

代码无法再运行了:因为 (1) 和 (2) 的 this 都是 longEar,在 (2) 处形成了死循环。
所以,对象的原型不能用 this 来查找,那怎么在子类的方法中获取父类的原型呢?最简单直接的方式就是把它存起来。

为了解决这个问题,JS 为函数增加了一个特殊的内部属性:[[HomeObject]]

当一个函数被定义为《类方法》或者《对象方法》时,它的 [[HomeObject]] 属性就成为了该对象
[[HomeObject]] 不能被更改,这个绑定是永久的
然后 super 可以使用它来解析父原型及其方法

多重继承

JS 不支持多重继承,但是可以通过将方法拷贝到类的 prototype 中来实现 mixin

对比

class 只是语法糖,相对于传统构造函数进行对比:

class构造函数
constructor构造函数本身
类方法原型上的方法
类字段构造函数中的属性初始化
静态属性/方法构造函数本身添加的属性/方法
私有属性/方法闭包
extends原型继承
supercall 或 apply