类
1. 类和原型
- 在 JavaScript 中,类使用基于原型的继承。
- 如果两个对象从同一个原型继承属性,就称这两个对象是同一个类的实例。
- 类意味着一组对象从同一个原型对象继承属性,因此,原型对象是类的核心。
- 通常通过一个函数来创建和初始化新对象,如:
javascript
// 工厂函数用于创建对象
function range(from, to) {
let r = Object.create(range.methods);
r.from = from;
r.to = to;
return r;
}
// 定义原型对象,由所有的range类实例继承
range.methods = {
includes(x) {
return this.from <= x && x <= this.to;
},
*[Symbol.iterator]() {
for (let x = Math.ceil(this.from); x <= this.to; x++) yield x;
},
toString() {
return "(" + this.from + "..." + this.to + ")";
}
}
let r = range(1, 3);
r.includes(2) // true
[...r] // [1, 2, 3]
注意
range()
是一个工厂函数,用于创建新的range
对象。- 使用
range.methods
保存这个类的原型对象,不是习惯写法。 range()
为每个对象定义了from
和to
属性,这两个属性非共享,非继承。- 使用
Symbol.iterator
定义了一个迭代器,方法名前有*
,表明是一个生成器函数。 - 使用
this
是所有类方法的特征。
2. 类和构造函数
- 构造函数是一种专门用于创建对象的函数。
- 构造函数需要使用
new
关键字调用,会自动创建对象,因此构造函数本身只需要初始化新对象的状态。 - 构造函数的
prototype
属性将用作新对象的原型,因此使用同一个构造函数创建的所有对象都继承自同一个对象,是同一个类的成员。 - 只有函数对象才有
prototype
属性,但箭头函数没有。
javascript
function Range(from, to) {
this.from = from;
this.to = to;
}
// 原型对象必须给 prototype 属性
Range.prototype = {
includes(x) {
return this.from <= x && x <= this.to;
},
*[Symbol.iterator]() {
for (let x = Math.ceil(this.from); x <= this.to; x++) yield x;
},
toString() {
return "(" + this.from + "..." + this.to + ")";
}
}
let r = range(1, 3);
r.includes(2) // true
[...r] // [1, 2, 3]
注意
- 构造函数按约定函数名首字母大写。
- 构造函数中不需要显示返回新创建的对象,它调用会自动创建新对象,并将构造函数作为新对象的方法来调用,然后返回新对象。
- 构造函数调用时会自动把
prototype
属性作为新对象的原型。 - 构造函数及原型中的方法不能使用箭头函数定义。
额外知识点:
- 构造函数内部可以通过
new.target
表达式判断该函数是否作为构造函数被调用了,如果该表达式有值,就是作为构造函数被调用,否则就是普通函数调用,没有使用new
关键字。 - 如果想让构造函数可以不使用
new
关键字创建对象,可以这样编码:
javascript
function Range(from, to) {
if (!new.target) return new Range(from, to);
// 初始化代码
this.from = from;
this.to = to;
}
2.1 构造函数、类标识和 instanceof
- 构造函数不是类的基本标识,因为不同的构造函数的
prototype
可能指向同一个对象,都能创建同一个类的实例。 - 原型对象才是类的基本标识,构造函数是类的外在表现。
instanceof
操作符左侧操作数应是要检测的对象,右侧操作数应该是某个类的构造函数。instanceof
会照原型链向上查找,只要右侧构造函数的prototype
存在于原型链中,就返回true
。instanceof
并非检查左侧对象是否是右侧构造函数创建的,而是检查是否继承了右侧构造函数的prototype
对象。
javascript
function Strange() {}
Strange.prototype = Range.prototype;
new Strange() instanceof Range // true
2.2 constructor 属性
- 普通函数自动拥有一个
prototype
属性,值是一个对象,因此可以不用给该属性显式赋值一个对象。 - 普通函数自动拥有的
prototype
对象有一个不可枚举的constructor
属性,值就是函数对象本身。
javascript
let F = function() {}
F.prototype.constructor === F // true
- 由构造函数创建的对象也会继承
constructor
属性,返回该对象的类。
javascript
const f = new F();
f.constructor === F // true
- 如果利用新对象覆盖
prototype
属性,需要显式地添加constructor
对象。
javascript
Range.prototype = {
// 显式设置constructor
constructor: Range,
/** 以下是其它方法 */
}
3. 使用 class 关键字的类
- 新增
class
关键字并未改变 JavaScript 类基于原型的本质。 - 使用
class
定义类更方便,可以把它看成更基础类定义机制的语法糖。
javascript
class Range {
constructor(from, to) {
// 这些属性为对象独有,非共享
this.from = from;
this.to = to;
}
includes(x) {
return this.from <= x && x <= this.to;
}
*[Symbol.iterator]() {
for (let x = Math.ceil(this.from); x <= this.to; x++) yield x;
}
toString() {
return "(" + this.from + "..." + this.to + ")";
}
}
let r = range(1, 3);
r.includes(2) // true
[...r] // [1, 2, 3]
说明
- 类以
class
关键字声明,后跟类名和花括号中的类体。 - 类体中方法以简写形式声明,方法之间没有逗号,与对象中声明有点区别。
constructor
用于定义类的构造函数,实际上会定义一个与类名相同的新变量,并将该函数赋值给新变量。- 如果类不需要初始化,可以不写构造函数,解释器会自动创建一个空构造函数。
- 使用
extends
关键字可以继承另一个类。
javascript
class Span extends Range {
constructor(start, length) {
if (length > 0) {
super(start, start + length);
} else {
super(start + length, start);
}
}
- 与函数声明类似,类也可以通过语句或表达式声明。类的表达式声明也可以包括可选的类名,该类名只能在类体内部访问。类的表达式声明并不常用:
javascript
let Square = class { constructor(x) { this.area = x * x } };
new Square(3).area // 9
注意
- 即使没有出现
"use strict"
指令,class
声明体中所有代码默认处于严格模式。 - 与函数声明不同,类声明不会提升。
3.1 静态方法
- 在
class
体中,把static
关键字放在方法声明前面可以定义静态方法。 - 静态方法是作为构造函数而非原型对象的属性定义的。
javascript
class Range {
...
static parse(s) {
let matches = s.match(/^\((\d+)\.\.\.(\d+)\)$/);
if(!matches) {
throw new TypeError(`Cannot parse Range from "${s}".`)
}
return new Range(parseInt(matches[1]), parseInt(matches[2]));
}
}
let r = Range.parse('(1...10)') // 返回新Range对象
r.parse('(1...10)') // TypeError
3.2 获取方法,设置方法及其它形式方法
- 在
class
体内,可以像在对象字面量中一样定义获取方法和设置方法,唯一区别就是类体内方法后不加逗号。 - 一般来说,对象字面量支持的所有简写方法都可以在类体中使用,包括生成器方法和名为表达式的方法。
3.3 公有、私有和静态字段
- 在较新的 ECMAScript 版本(至少是 ES6 之后)中可以使用字段声明语法,字段可以不用写在构造函数中:
javascript
// 新字段声明语法
class Buffer {
size = 0;
capacity = 4096;
// 虽然左操作数中this省略了,但在右操作数中使用时不能省略
buffer = new Unit8Array(this.capacity);
}
// 等价于
class Buffer {
constructor() {
this.size = 0;
this.capacity = 4096;
this.buffer = new Unit8Array(this.capacity);
}
}
- 声明时可以不初始化,值为
undefined
。 - 在字段名前加上
#
,则该字段只能在类体内部使用,对类外部的任何代码都不可见,不可访问:
javascript
class Buffer {
size = 0;
capacity = 4096;
// 私有字段
#buffer = new Unit8Array(this.capacity);
}
- 私有字段必须先使用字段声明语法才能使用。
- 在字段前加上
static
关键字,这些字段会被创建为构造函数的属性,而非实例属性。在以前,静态字段只能在类体外,定义类之后定义:
javascript
class Buffer {
size = 0;
// 静态字段,只能通过 Buffer.capacity 访问
static capacity = 4096;
#buffer = new Unit8Array(this.capacity);
}
3.4 示例
javascript
class Complex {
static ZERO = new Complex(0, 0);
// 使用私有字段保存实数和虚数部分
#real = 0;
#imaginary = 0;
constructor(real, imaginary) {
this.#real = real;
this.#imaginary = imaginary;
}
set real(v) {
this.#real = v;
}
set imaginary(v) {
this.#imaginary = v;
}
get real() {
return this.#real;
}
get imaginary() {
return this.#imaginary;
}
plus(that) {
return new Complex(this.#real + that.real, this.#imaginary + that.imaginary);
}
times(that) {
return new Complex(this.#real * that.real - this.#imaginary * that.imaginary,
this.#real * that.imaginary + this.#imaginary * that.real);
}
static sum(c, d) {
return c.plus(d);
}
static product(c, d) {
return c.times(d);
}
// 每个类都应该有个toString()
toString() {
return `${this.#real}, ${this.#imaginary}`;
}
// 测试两个实例是否相等
equals(that) {
return that instanceof Complex && this.#real === that.real &&
this.#imaginary === that.imaginary;
}
}
let c = new Complex(2, 3);
let d = new Complex(c.imaginary, c.real);
c.plus(d).toString() // {5, 5}
Complex.product(c, d).toString() // {0, 13}
Complex.ZERO.toString() // {0, 0}
4. 为已有类添加方法
- JavaScript 基于原型的继承机制是动态的。
- 只要给原型对象添加方法,就可以增强 JavaScript 类。
- 不建议给内置类型的原型对象增加方法,可能导致兼容问题 。
javascript
Complex.prototype.conj = function() {
return new Complex(this.#real, -this.#imaginary);
}
5. 子类
5.1 子类与原型
在 class
关键字之前,想要实现继承,需要让子类的原型对象继承父类的原型对象:
javascript
// 子类的构造函数
function Span(start, span) {
if (span >= 0) {
this.from = start;
this.to = start + span;
} else {
this.to = start;
this.from = start + span;
}
}
// 继承父类原型
Span.prototype = Object.create(Range.prototype);
// 定义constructor
Span.prototype.constructor = = Span;
// 覆盖父类的toString()
Span.prototype.toString = function() {
return `(${this.from}...${this.to - this.from})`
}
5.2 通过 extends 和 super 创建子类
在 ES6 之后,可以通过 extends
关键字继承父类,通过 super
调用父类构造函数和方法:
javascript
class TypedMap extends Map {
constructor(keyType, valueType, entries) {
// 如果制定了键值对,则检查类型
if (entries) {
for (let [k, v] of entries) {
if (typeof k !== keyType || typeof v !== valueType) {
throw new TypeError(`Wrong type for entry [${k}, ${v}]`)
}
}
// 初始化父类
super(entries);
// 初始化子类
this.keyType = keyType;
this.valueType = valueType;
}
// 重写set方法
set(key, value) {
if (this.keyType && typeof key !== this.keyType) {
throw new TypeError(`${key} is not of type ${this.keyType}`)
}
if (this.valueType && typeof value !== this.valueType) {
throw new TypeError(`${value} is not of type ${this.valueType}`)
}
// 如果类型正确,调用父类方法
return super.set(key, value);
}
}
}
注意
- 如果使用
extends
关键字定义类,则类的构造函数中必须显式通过super()
调用父类构造函数。 - 如果没有在子类中定义构造函数,解释器会自动创建。同时,构造函数会取得传入的值并传给
super()
。 - 在通过
super()
调用父类构造函数之前,不能在构造函数中使用this
关键字。 - 在构造函数中,
new.target
引用的是被调用的构造函数,当子类构造函数被调用并通过super
调用父类构造函数时,父类构造函数中的new.target
指向的是子类的构造函数。
5.3 委托而不是继承
- 当一个类与另一个类有相同的行为,可以通过子类继承该行为,也可以在类中创建该类的实例,委托该实例对象去执行某些行为,这时候不需要创建子类,只需要组合其他类。
- 一个准则:能组合就不继承。
5.4 类层次与抽象类
- 抽象父类可以定义部分实现供所有子类继承和共享,子类需要实现父类定义的抽象方法。
- JavaScript 没有正式定义抽象方法或抽象类的语法。
javascript
// 抽象类
class AbstractSet {
// 抛出错误,强制子类必须定义该方法
has(x) {
throw new Error("Abstract method")
}
}
// AbstractSet的抽象子类
class AbstractEnumerableSet extends AbstractSet {
// 抽象获取方法
get size() { throw new Error("Abstract method") }
// 抽象迭代器
*[Symbol.iterator]() { throw new Error("Abstract method") }
isEmpty() { return this.size === 0 }
toString() { return `${Array.from(this).join(',')}` }
equals(set) {
// 如果另一个集合不是AbstractEnumerableSet,那肯定不相等
if(!(set instanceof AbstractEnumerableSet)) return false;
// 如果大小不一样,肯定不相等
if(this.size !== set.size) return false;
// 循环检查元素
for(let element of this) {
if(!set.has(element)) return false;
}
return true;
}
}
// AbstractEnumerableSet 的抽象子类
class AbstractWritableSet extends AbstractEnumerableSet {
insert(x) { throw new Error("Abstract method") }
remove(x) { throw new Error("Abstract method") }
add(set) {
for(let element of set) {
this.insert(element)
}
}
subtract(set) {
for(let element of set) {
this.remove(element)
}
}
interset(set) {
for(let element of this) {
if(!set.has(element)) {
this.remove(element)
}
}
}
}