《javascript高级程序设计》学习笔记 | 8.4.类
关注前端小讴,阅读更多原创技术文章
类
- ES6 新引入
class
关键字具有正式定义类的能力,其背后使用的仍然是原型和构造函数的概念
类定义
- 与函数类型类似,定义类也有 2 种主要方式:类声明和类表达式,2 种方式都是用
class
关键字加大括号
class Person {} // 类声明
var animal = class {} // 类表达式
- 类表达式在被求值前不能引用(同函数表达式),类定义不能声明提升(与函数表达式不同)
console.log(FunctionDeclaration) // [Function: FunctionDeclaration],函数声明提前
function FunctionDeclaration() {}
console.log(ClassDeclaration) // ReferenceError: Cannot access 'ClassDeclaration' before initialization,类没有声明提前
class ClassDeclaration {}
- 类受块级作用域限制(函数受函数作用域限制)
{
function FunctionDeclaration2() {}
class ClassDeclaration2 {}
}
console.log(FunctionDeclaration2) // [Function: FunctionDeclaration2]
console.log(ClassDeclaration2) // ReferenceError: ClassDeclaration2 is not defined
类的构成
- 类可以包含构造函数方法、实例方法、获取函数、设置函数和静态类方法,也可以空类定义
- 默认情况下,类定义中的代码都在严格模式下执行
- 类名建议首字母大写(与构造函数一样),以区分类和实例
class Foo {} // 空类定义
class Bar {
constructor() {} // 构造函数
get myBaz() {} // 获取函数
static myQux() {} // 静态方法
}
- 把类表达式赋值给变量后,可以在类表达式作用域内部访问类表达式,并通过
name
属性取得类表达式的名称
var Person2 = class PersonName {
identify() {
console.log(PersonName) // 类表达式作用域内部,访问类表达式
console.log(Person2.name, PersonName.name) // 类表达式作用域内部,访问类表达式的名称
/*
[class PersonName]
PersonName PersonName
*/
}
}
var p = new Person2()
p.identify()
console.log(Person2.name) // PersonName
console.log(PersonName) // ReferenceError: PersonName is not defined,类表达式作用域外部,无法访问类表达式
类构造函数
- 在类定义块内部用
constructor
关键字创建类的构造函数:- 使用
new
操作符创建类的实例时,调用constructor
方法 - 构造函数的定义非必需,不定义构造函数相当于将构造函数定义为空函数
- 使用
实例化
- 使用
new
操作符调用类的构造函数会执行如下操作(同构造函数):- 创建了一个新对象(实例)
- 新对象内部的
[[Prototype]]
特性被赋值为构造函数的prototype
属性(共同指向原型) - 将构造函数的作用域(即 this)赋给新对象
- 执行构造函数中的代码(即:为这个对象添加新属性)
- 返回新对象或非空对象
class Animal2 {}
class Person3 {
constructor() {
console.log('person ctor')
}
}
class Vegetable {
constructor() {
this.color = 'orange'
}
}
var a = new Animal2()
var p = new Person3() // 'person ctor'
var v = new Vegetable()
console.log(v.color) // 'orange'
- 类实例化时,传入的参数会用做构造函数的参数(无参数则类名后可不加括号)
class Person4 {
constructor(name) {
console.log(arguments.length)
this.name = name || null
}
}
var p1 = new Person4() // 0,无参数,Person4后的括号也可省略
console.log(p1.name) // null
var p2 = new Person4('Jake') // 1
console.log(p2.name) // 'Jake'
- 默认情况下,类构造函数会在执行后返回
this
对象(类实例),如果返回的不是this
对象,则该对象用instanceof
操作符检测时与类无关联
class Person5 {
constructor() {
this.foo = 'foo'
}
}
var p3 = new Person5()
console.log(p3) // Person5 { foo: 'foo' },类实例
console.log(p3.__proto__) // {},类原型
console.log(p3.constructor) // [class Person5],类本身当作构造函数
console.log(Person5 === p3.constructor) // true
console.log(Person5.prototype === p3.__proto__) // true
console.log(p3 instanceof Person5) // true,p3是Person5的实例(Person5.prototype在p3的原型链上)
class Person6 {
constructor() {
return {
bar: 'bar', // 返回一个全新的对象(不是该类的实例)
}
}
}
var p4 = new Person6()
console.log(p4) // { bar: 'bar' },不是Person6的类实例
console.log(p4.__proto__) // {},Object原型
console.log(p4.constructor) // [Function: Object],Object构造函数
console.log(Object === p4.constructor) // true
console.log(Object.prototype === p4.__proto__) // true
console.log(p4 instanceof Person6) // false,p4不是Person6的实例(Person6.prototype不在p4的原型链上)
- 类构造函数与构造函数的主要区别是:类构造函数必须使用
new
操作符,普通构造函数可以不使用new
操作符(当作普通函数调用)
function Person7() {} // 普通构造函数
class Animal3 {} // 类构造函数
var p5 = Person7() // 构造函数不使用new操作符,当作普通函数调用
var a1 = Animal3() // TypeError: Class constructor Animal3 cannot be invoked without 'new',类构造函数必须使用new操作符实例化
var a2 = new Animal3()
- 类构造函数实例化后,会成为普通的实例方法,可以使用
new
操作符在实例上引用它
class Person8 {
constructor() {
console.log('foo')
}
}
var p6 = new Person8() // 'foo'
// p6.constructor() // TypeError: Class constructor Person8 cannot be invoked without 'new'
new p6.constructor() // 'foo'
把类当成特殊函数
- 类是一种特殊函数,可用
typeof
操作符检测
console.log(typeof Person8) // function
- 类标识具有
prototype
属性(指向原型),原型的constructor
属性(默认)指向类自身
console.log(Person8.prototype) // {},原型
console.log(Person8.prototype.constructor) // [class Person8],类自身
console.log(Person8.prototype.constructor === Person8) // true
- 可以使用
instanceof
操作符检查类prototype
指向的对象是否存在于类实例的原型链中
console.log(p6 instanceof Person8) // true,p6是Person8的实例
- 使用
new
操作符调用类本身时,类本身被当成构造函数,类实例的constructor
指向类本身 - 使用
new
操作符调用类构造函数时,**类构造函数(constructor()
)**被当成构造函数,类实例的constructor
指向 Function 构造函数
class Person9 {}
console.log(Person9.constructor) // [Function: Function],指向Function原型的constructor,即Function构造函数
var p7 = new Person9() // new调用类本身,类本身被当作构造函数
console.log(p7.constructor) // [class Person9],constructor指向构造函数,即类本身
console.log(p7.constructor === Person9) // true
console.log(p7 instanceof Person9) // true,p7是Person9的实例
var p8 = new Person9.constructor() // new调用类构造函数,类构造函数(constructor())被当作构造函数
console.log(p8.constructor) // [Function: Function],constructor指向构造函数,即Function构造函数
console.log(p8.constructor === Function) // true
console.log(p8 instanceof Person9.constructor) // true,p8是Person9.constructor的实例
- 可以把类当作参数传递
let classList = [
class {
constructor(id) {
this._id = id
console.log(`instance ${this._id}`)
}
},
]
function createInstance(classDefinition, id) {
return new classDefinition(id)
}
var foo = new createInstance(classList[0], 3141) // 'instance 3141'
- 类可以立即实例化
var p9 = new (class Foo2 {
constructor(x) {
console.log(x) // 'bar'
}
})('bar')
console.log(p9) // Foo2 {},类实例
console.log(p9.constructor) // [class Foo2],类本身
实例、原型和类成员
- 类的语法可以非常方便定义应该存在于实例上、原型上及类本身的成员
实例成员
- 在类的构造函数(
constructor()
)内部,可以为实例添加自有属性 - 每个实例对应唯一的成员对象,所有成员不会在原型上共享
class Person10 {
constructor() {
this.name = new String('Jack')
this.sayName = function () {
console.log(this.name)
}
this.nickNames = ['Jake', 'J-Dog']
}
}
var p10 = new Person10()
var p11 = new Person10()
console.log(p10.name) // [String: 'Jack'],字符串包装对象
console.log(p11.name) // [String: 'Jack'],字符串包装对象
console.log(p10.name === p11.name) // false,非同一个对象(不共享)
console.log(p10.sayName) // ƒ () { console.log(this.name) },function对象
console.log(p11.sayName) // ƒ () { console.log(this.name) },function对象
console.log(p10.sayName === p11.sayName) // false,非同一个对象(同理,不共享)
console.log(p10.nickNames === p11.nickNames) // false,同理↑
p10.name = p10.nickNames[0]
p11.name = p10.nickNames[1]
p10.sayName() // 'Jake',实例成员互不影响
p11.sayName() // 'J-Dog',实例成员互不影响
原型方法与访问器
- 在类块(
{}
)中定义的方法作为原型方法
class Person11 {
constructor() {
// 实例方法
this.locate = () => {
console.log('instance')
}
}
locate() {
// 原型方法
console.log('prototype')
}
locate2() {
// 原型方法
console.log('prototype2')
}
}
var p12 = new Person11()
p12.locate() // 'instance',实例方法遮盖原型方法
p12.__proto__.locate() // 'prototype'
p12.locate2() // 'prototype2'
- 方法可以定义在类的构造函数或类块中,属性不能定义在类块中
class Person12 {
name: 'jack' // Uncaught SyntaxError: Unexpected identifier
}
- 可以使用字符串、符号或计算的值,作为类方法的键
const symbolKey = Symbol('symbolKey')
class Person13 {
stringKey() {
// 字符串作为键
console.log('stringKey')
}
[symbolKey]() {
// 符号作为键
console.log('symbolKey')
}
['computed' + 'Key']() {
// 计算的值作为建
console.log('computedKey')
}
}
- 在类块(
{}
)中定义获取和设置访问器
class Person14 {
set setName(newName) {
this._name = newName
}
get getName() {
return this._name
}
}
var p13 = new Person14()
p13.setName = 'Jake'
console.log(p13.getName) // 'Jake'
静态类方法
- 在类块(
{}
)中定义静态类方法,方法存在于类本身上 - 每个类只能有一个静态类成员
class Person15 {
constructor() {
// 添加到this的内容存在于不同实例上
this.locate = () => {
console.log('instance', this) // 此处的this为类实例
}
}
locate() {
// 定义在类的原型对象上
console.log('prototype', this) // 此处的this为类原型
}
static locate() {
// 定义在类本身上
console.log('class', this) // 此处的this为类本身
}
}
var p14 = new Person15()
p14.locate() // 'instance' Person15 { locate: [Function (anonymous)] }
p14.__proto__.locate() // 'prototype' {}
Person15.locate() // 'class' [class Person15]
- 静态类方法非常适合作为实例工厂
class Person16 {
constructor(age) {
this._age = age
}
sayAge() {
console.log(this._age)
}
static create() {
return new Person16(Math.floor(Math.random() * 100))
}
}
console.log(Person16.create()) // Person16 { _age: ... }
非函数原型和类成员
- 可以在类定义外部,手动地在原型和类上添加成员数据
- 类定义没有显示支持添加数据成员的方法,实例应该独自拥有通过
this
引用的数据
class Person17 {
sayName() {
console.log(`${Person17.greeting} ${this.name}`)
}
}
var p15 = new Person17()
Person17.greeting = 'My name is' // 在类上定义数据
Person17.prototype.name = 'Jake' // 在原型上定义数据
p15.sayName() // 'My name is Jake'
迭代器与生成器方法
- 可在原型和类本身上定义生成器方法
class Person18 {
*createNicknameIterator() {
// 在原型上定义生成器方法
yield 'Jack'
yield 'Jake'
yield 'J-Dog'
}
static *createJobIterator() {
// 在类本身定义生成器方法
yield 'Butcher'
yield 'Baker'
yield 'Candlestick maker'
}
}
var jobIter = Person18.createJobIterator() // 调用生成器函数,产生生成器对象
console.log(jobIter.next().value) // 'Butcher'
console.log(jobIter.next().value) // 'Baker'
console.log(jobIter.next().value) // 'Candlestick maker'
var p16 = new Person18()
var nicknameIter = p16.createNicknameIterator() // 调用生成器函数,产生生成器对象
console.log(nicknameIter.next().value) // 'Jack'
console.log(nicknameIter.next().value) // 'Jake'
console.log(nicknameIter.next().value) // 'J-Dog'
- 生成器方法可作为默认迭代器,把类实例变成可迭代对象
class Person19 {
constructor() {
this.nickNames = ['Jack', 'Jake', 'J-Dog']
}
*[Symbol.iterator]() {
// 生成器函数作为默认迭代器
yield* this.nickNames.entries()
}
}
var p17 = new Person19()
for (let [i, n] of p17) {
console.log(i, n)
/*
0 'Jack'
1 'Jake'
2 'J-Dog'
*/
}
- 可直接返回迭代器实例
class Person20 {
constructor() {
this.nickNames = ['Jack', 'Jake', 'J-Dog']
}
[Symbol.iterator]() {
// 返回迭代器实例
return this.nickNames.entries()
}
}
var p18 = new Person20()
for (let [i, n] of p18) {
console.log(i, n)
/*
0 'Jack'
1 'Jake'
2 'J-Dog'
*/
}
继承
- ES6 支持类继承机制,使用的依旧是原型链
继承基础
- ES6 类支持单继承,使用
extends
关键字可以继承任何拥有[[Construct]]
和原型的对象(可继承类或普通构造函数)
class Vehicle {}
class Bus extends Vehicle {} // 继承类
var b = new Bus()
console.log(b instanceof Bus) // true
console.log(b instanceof Vehicle) // true
function Person21() {}
class Engineer extends Person21 {} // 继承构造函数
var p19 = new Engineer()
console.log(p19 instanceof Engineer) // true
console.log(p19 instanceof Person21) // true
extends
关键字可在类表达式中使用
var Bus2 = class extends Vehicle {}
- 子类通过原型链访问父类和父类原型上定义的方法,
this
值反映调用相应方法的实例或类
class Vehicle2 {
identifyPrototype(id) {
// 父类原型上定义的方法
console.log(id, this)
}
static identifyClass(id) {
// 父类本身定义的方法
console.log(id, this)
}
}
class Bus3 extends Vehicle2 {}
var v = new Vehicle2()
var b2 = new Bus3()
v.identifyPrototype('v') // 'v' Vehicle2 {},this为父类实例
b2.identifyPrototype('b') // 'b' Bus3 {},this为子类实例
v.__proto__.identifyPrototype('v') // 'v' {},this为父类原型
b2.__proto__.identifyPrototype('b') // 'b' Vehicle2 {},this为子类原型,即父类实例
Vehicle2.identifyClass('v') // v [class Vehicle2],this为父类本身
Bus3.identifyClass('b') // b [class Bus3 extends Vehicle2],this为子类本身
构造函数、HomeObject 和 super()
- 在子类构造函数中,可以通过
super()
调用父类构造函数
class Vehicle3 {
constructor() {
this.hasEngine = true
}
}
class Bus4 extends Vehicle3 {
constructor() {
super() // 调用父类构造函数constructor
console.log(this) // Bus4 { hasEngine: true },子类实例,已调用父类构造函数
console.log(this instanceof Vehicle3) // true
console.log(this instanceof Bus4) // true
}
}
new Bus4()
- 在子类静态方法,可以通过
super()
调用父类静态方法
class Vehicle4 {
static identifyV() {
// 父类静态方法
console.log('vehicle4')
}
}
class Bus5 extends Vehicle4 {
static identifyB() {
// 子类静态方法
super.identifyV() // 调用父类静态方法
}
}
Bus5.identifyB() // 'vehicle4'
-
ES6 给类构造函数和静态方法添加了内部特性
[[HomeObject]]
,指向定义该方法的对象,super
始终会定义为[[HomeObject]]
的原型 -
使用
super
需注意几个问题:super
只能在子类构造函数和子类静态方法中使用- 不能单独引用
super
关键字,要么调用构造函数,要么引用静态方法 - 调用
super()
会调用父类构造函数,并将返回的实例赋值给this
- 调用
super()
时如需给父类构造函数传参,需手动传入 - 若子类未定义构造函数,则其实例化时**自动调用
super()
**并传入所有传给父类的参数 - 调用
super()
前,不能在子类构造函数或静态方法里先引用this
- 若子类显式定义构造函数,则要么在其中调用
super()
,要么在其中返回新对象
class Vehicle5 {
constructor(id) {
// super() // SyntaxError: 'super' keyword unexpected here,super只能在子类构造函数和子类静态方法中使用
this.id = id
}
}
class Bus6 extends Vehicle5 {
constructor(id) {
// console.log(super) // SyntaxError: 'super' keyword unexpected here,不能单独引用super
// console.log(this) // ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor,调用super()之前不能引用this
super(id) // 调用父类构造函数,手动给父类构造函数传参,并将返回的实例赋给this(子类实例)
console.log(this) // Bus6 { id: 5 },子类实例
console.log(this instanceof Vehicle5) // true
console.log(this instanceof Bus6) // true
}
}
new Bus6(5)
class Bus7 extends Vehicle5 {} // 子类未定义构造函数
console.log(new Bus7(6)) // Bus7 { id: 6 },实例化时自动调用super()并传参
class Bus8 extends Vehicle5 {
// constructor() {} // ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor
constructor(id) {
super(id) // 子类显式定义构造函数,要么调用super()
}
}
class Bus9 extends Vehicle5 {
// constructor() {} // ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor
constructor(id) {
return {} // 子类显式定义构造函数,要么返回其他对象
}
}
console.log(new Bus8(7)) // Bus8 { id: 7 },子类实例
console.log(new Bus9(8)) // {},返回新对象
抽象基类
- 可设置父类仅供子类继承,本身不会被实例化,通过
new.target
检测是否为抽象基类 - 可在父类要求子类必须定义某方法,通过
this
检查相应方法是否存在
class Vehicle6 {
constructor() {
console.log(new.target)
if (new.target === Vehicle6) {
// 阻止抽象基类被实例化
throw new Error('Vehicle6 cannot be directly instantiated')
}
if (!this.foo) {
// 要求子类必须定义foo()方法
throw new Error('Inheriting class must define foo()')
}
}
}
class Bus10 extends Vehicle6 {} // 子类未定义foo()方法
class Bus11 extends Vehicle6 {
// 子类定义了foo()方法
foo() {}
}
// new Vehicle6() // [class Vehicle6],Error: Vehicle6 cannot be directly instantiated
// new Bus10() // [class Bus10 extends Vehicle6],Error: Inheriting class must define foo()
new Bus11() // [class Bus11 extends Vehicle6]
继承内置类型
- 可以使用类继承扩展内置类型
class SuperArray extends Array {
// 在子类原型上追加方法:任意洗牌
shuffle() {
for (let i = this.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1))
;[this[i], this[j]] = [this[j], this[i]]
}
}
}
var a = new SuperArray(1, 2, 3, 4, 5)
console.log(a instanceof Array) // true,a是Array的实例
console.log(a instanceof SuperArray) // true,a是SuperArray的实例
console.log(a) // SuperArray(5) [ 1, 2, 3, 4, 5 ]
a.shuffle()
console.log(a) // SuperArray(5) [ 3, 1, 2, 5, 4 ]
- 内置类型的有些方法返回新实例,默认情况下返回实例类型与原始实例类型一致
var a1 = new SuperArray(1, 2, 3, 4, 5)
var a2 = a1.filter((x) => !!(x % 2)) // filter方法返回新的实例,实例类型与a1的一致
console.log(a1) // SuperArray(5) [ 1, 2, 3, 4, 5 ]
console.log(a2) // SuperArray(3) [ 1, 3, 5 ]
console.log(a1 instanceof SuperArray) // true
console.log(a2 instanceof SuperArray) // true
- 可以用
Symbol.species
定义静态获取器getter
方法,覆盖内置类型的方法新创建实例时返回的类
class SuperArray2 extends Array {
// Symbol.species定义静态获取器方法,覆盖新创建实例时返回的类
static get [Symbol.species]() {
return Array // 内置类型的方法新创建实例时,返回Array类型
}
}
var a3 = new SuperArray2(1, 2, 3, 4, 5)
var a4 = a3.filter((x) => !!(x % 2)) // filter方法返回新的实例,实例类型已被覆盖(Array)
console.log(a3) // SuperArray(5) [ 1, 2, 3, 4, 5 ]
console.log(a4) // [ 1, 3, 5 ]
console.log(a3 instanceof SuperArray2) // true
console.log(a4 instanceof SuperArray2) // false
类混入
extends
关键字后面除了可以是父类,还可以是任何可以解析为一个类或构造函数的表达式
class Vehicle7 {}
function getParentClass() {
console.log('evaluated expression')
return Vehicle7 // 表达式被解析为Vehicle7类
}
class Bus12 extends getParentClass {}
new Bus12() // 'evaluated expression'
- 可以通过混入模式,在一个表达式中连缀多个混入元素,该表达式最终解析为一个可以被继承的类
class Vehicle8 {}
let FooMixin = (SuperClass) =>
// 表达式接收超类作为参数,返回子类
class extends SuperClass {
// 子类原型追加方法
foo() {
console.log('foo')
}
}
let BarMixin = (SuperClass) =>
// 表达式接收超类作为参数,返回子类
class extends SuperClass {
// 子类原型追加方法
bar() {
console.log('bar')
}
}
class Bus13 extends BarMixin(FooMixin(Vehicle8)) {} // 嵌套逐级继承:FooMixin继承Vehicle8,BarMixin继承FooMixin,Bus13继承BarMixin
var b3 = new Bus13()
console.log(b3) // Bus13 {},子类实例
b3.foo() // 'foo',继承了超类原型上方法
b3.bar() // 'bar',继承了超类原型上方法
- 通过写一个辅助函数,把嵌套调用展开
function mix(BaseClass, ...Mixins) {
/*
reduce接收2个参数:对每一项都会运行的归并函数、归并起点的初始值(非必填)
归并函数接收4个参数:上一个归并值、当前项、当前索引、数组本身
*/
return Mixins.reduce(
(pre, cur) => cur(pre), // 归并方法:执行当前项方法,参数为上个归并值
BaseClass // 归并初始值
)
}
class Bus14 extends mix(Vehicle7, FooMixin, BarMixin) {}
var b4 = new Bus14()
console.log(b4) // Bus14 {}
b4.foo() // 'foo'
b4.bar() // 'bar'
总结 & 问点
- 如何定义 JS 的类?类和函数有哪些异同?
- 类可以包含哪些内容?如何访问类表达式及其名称?
- 类实例化时内部历经哪些步骤?类构造函数默认返回什么?若返回一个新对象会有什么印象?
- 类构造函数与普通构造函数的主要区别是什么?
- 类属于什么数据类型?其 prototype 和 constructor 分别指向什么?使用 new 操作符调用类本身和类构造函数有什么区别?
- 写 2 段代码,分别表述“类当作参数”及“类立即实例化”
- 如何定义类的实例成员、原型方法、访问器方法、静态类方法?其又分别定义在类的什么位置(实例/原型/类本身)?
- 写 1 段代码,使用静态类方法作为实例工厂
- 如何手动添加类成员数据?为什么类定义中不显示支持添加数据成员?
- 生成器方法可以定义在类的什么位置?写 1 段代码,在类定义时添加生成器方法,并将其作为默认迭代器
- 类可以通过 extends 关键字继承哪些对象?extends 后面可以是哪些元素?子类通过原型链可以访问父类哪里的方法?
- super 关键字的作用和用法是什么?其使用时有哪些注意点?
- 如何定义抽象基类?其作用是什么?
- 写一段代码,用类继承扩展内置对象 Array,且子类调用 concat()方法后返回 Array 实例类型
- 写一段代码,用混入模式的嵌套调用实现多个类的逐级继承,再用辅助函数实现嵌套展开