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
但是还是有一些不同:
- 通过 class 创建的函数具有特殊的内部属性标记
[[IsClassConstructor]]: true
,所以只能通过 new 调用 - 类方法不可枚举
- 类中自动开启严格模式
特性
有函数的特性:类可以作为变量,也有类表达式
也有对象的特性:可以设置 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
在 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 | 原型继承 |
super | call 或 apply |