类
前面我们了解了怎么实现继承,但是各种方法都有自己的问题,代码也比较冗余和混乱,所以为了解决这些问题,ES6引入了class关键字,可以正式定义类,类表面上看起来可以正式面向对象编程,但是实际上背后还是原型和构造函数的概念,类只是一个新的基础性语法糖结构
类理论
在本文的开头,我们先来了解一下类的一些相关理念,而这个过程我们并不会使用JS的类来讲解
此处使用一个例子(伪代码),后续的类理论将基于这个例子解析:
class Vehicle {
engines = 1
ignition() {
output('启动引擎')
}
drive(){
ignition()
output('开始行驶')
}
}
class Car inherits Vehicle {
wheels = 4
drive(){
inherited: drive()
output('上马路了', wheels, '个轮子')
}
}
class Boat inherits Vehicle {
engines = 2
ignition(){
output('启动引擎', engines, '个引擎')
}
pilot(){
inherited: drive()
output('下水航行')
}
}
继承
在面向类的语言中,可以先定义一个类,然后定义一个继承前者的类
定义好子类之后,子类对于父类来说,就是一个独立并完全不同的类,子类会包含父类行为的原始副本,也可以重写所有继承的行为甚至定义新行为
在上述例子中,Car和Boat都是继承Vehicle的,都拥有Vehicle的engines、ignition等属性和方法
多态
定义:同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果
在上面的例子中,我们可以发现,Car重写了父类Vehicle的drive方法,但是也在后面调用了inherited: drive()方法,这表明Car可以引用并重写继承自父类的原始方法drive()
这种技术就被称作多态,在本例中,具体来说叫做相对多态
相对:这只是多态的一个方面,任何方法都可以引用继承层次中高层的方法,说相对是因为我们并不会定义想要访问的绝对继承层次,而只是相对的查找上一层
许多语言中使用**super来代替本例子中的inherited,含义是超类**,表示当前类的父类/祖先类
混入
JS中原来是不存在可以被实例化的类的,一个对象并不会被赋值到其他对象(继承)
所以JS开发者也想出一个方法来模拟继承,那就是混入
显式混入
我们手动实现一个继承行为,也就是mixin
function mixin(sourceObj, targetObj) {
for(const key in sourceObj){
//已经存在的属性或函数就不会进行重写,从而保留子类的同名属性
//实现了子类对父类的属性或方法重写
if(!(key in targetObj)) {
targetObj[key] = sourceObj[key]
}
}
return targetObj
}
var Vehicle = {
engines: 1,
ignition: function() {
console.log('启动引擎')
},
drive: function(){
this.ignition()
console.log('开始行驶')
}
}
//Car继承自Vehicle,并新增属性和方法
var Car = mixin(Vehicle, {
wheels: 4,
drive: function(){
Vehicle.drive.call(this)
console.log('上马路了' + wheels + '个轮子')
}
})
在这段代码中,我们可以重点看一下Vehicle.drive.call(this)这一段代码,这就是显式多态
因为我们需要显式指定调用的对象,因为如果不这样做的话,直接执行Vehicle.drive()则会将this绑定到Vehicle对象而不是Car对象
这种显式多态会极大的增加维护成本,所以一般不使用
显示混入还有一种变体,称为寄生继承,它既是显式的又是隐式的:
先复制一份父类的定义,然后混入子类的定义,然后用这个符合对象构建实例
function Vehicle() {
this.engines = 1
}
Vehicle.prototype.ignition = function() {
console.log('启动引擎')
}
Vehicle.prototype.drive = function() {
this.ignition()
console.log('开始行驶')
}
function Car() {
var car = new Vehicle()
car.wheels = 4
var vehDrive = car.drive //保存Vehicle的drive的特殊引用
car.drive = function() {
vehDrive.call(this)
console.log('使用Car行驶')
}
return car
}
var myCar = Car()
myCar.drive()
隐式混入
隐式混入也和之前的显式混入一样,都存在同样的问题
所谓隐式,实际上就是借用了函数Something.cool并在Another的上下文中同通过this绑定调用它,把Something的行为混入到Another中
var Something = {
cool: function(){
this.greeting = 'Hello world'
this.count = this.count ? this.count + 1 : 1
}
}
Something.cool()
console.log(Something.greeting)
console.log(Something.count)
var Another = {
cool: function(){
Something.cool.call(this)
}
}
Another.cool()
console.log(Another.greeting)
console.log(Another.count)
类定义
类定义
定义类的方法有两种:类声明和类表达式
class Person {}
const Animal = class {}
与函数表达式类似,类表达式在求值之前也不能被引用
console.log(FunctionExpression) //undefined
var FunctionExpression = function() {}
console.log(FunctionExpression) //function(){}
console.log(ClassExpression) //undefined
var ClassExpression = class {}
console.log(ClassExpression) //class {}
但是,与函数定义不同,函数声明可以提升,但是类定义不能提升
console.log(FunctionDeclaration) //FunctionDeclaration(){}
function FunctionDeclaration(){}
console.log(FunctionDeclaration) //FunctionDeclaration(){}
console.log(ClassDeclaration)
//Uncaught ReferenceError: Cannot access 'ClassDeclaration' before initialization
class ClassDeclaration {}
console.log(ClassDeclaration)
另外,跟函数声明不同的地方在于 :函数受函数作用域限制,类受块作用域限制
{
function FunctionDeclaration(){}
class ClassDeclaration {}
}
console.log(FunctionDeclaration) //FunctionDeclaration(){}
console.log(ClassDeclaration) //Uncaught ReferenceError: ClassDeclaration is not defined
类构成
类可以包含构造函数、实例方法、获取函数、设置函数、静态类方法等,但是这些都不是必须的,空的类照样有效
类名的首字母一般要大写,区别于通过它创建的实例
类表达式的名称是可选的,在把类表达式赋值给变量后,可以通过name属性来取得类表达式的名称字符串,但是不能在类表达式作用域外部访问这个标识符
let Person = class PersonName {
identify(){
console.log(Person.name, PersonName.name)
}
}
let p = new Person()
p.identify() //PersonName PersonName
console.log(Person.name) //PersonName
console.log(PersonName) //Uncaught ReferenceError: PersonName is not defined
类构造函数
constructor关键字用于在类定义块内部创建类的构造函数
在使用new操作符创建类的实例时,会调用constructor这个函数
constructor函数定义不是必须的,如果不定义这个函数则相当于将其定义为空函数
实例化
使用new操作符实例化Person相当于使用new调用其构造函数constructor
使用new操作符调用构造函数会发生一下几件事:
- 在内存中创建一个新的对象
- 这个对象的内部指针
[[Prototype]]被赋值为构造函数的prototype属性- 构造函数内部的
this被赋值为这个新对象- 执行构造函数内部的代码,给新对象添加属性
- 如果构造函数返回非空对象,则返回该对象,否则返回新创建的对象
class Person {
constructor(){
console.log('Person ctor')
}
}
let p = new Person() //Person ctor
如果实例化的时候不需要传参数,则类名后面的括号可以省略
class Person {
constructor(){
console.log('Person ctor')
}
}
let p = new Person //Person ctor
默认情况下,类构造函数会在执行之后返回this对象,构造函数返回的对象会被用做实例化的对象,如果没有什么引用新创建的this对象,那么这个对象会被销毁
如果返回的对象不是this对象,而是其他对象,那么这个对象不会通过instanceof操作符检测出跟类有关联,因为这个对象的原型指针并没有被修改
class Person{
constructor(override) {
this.foo = 'foo'
if(override){
return {
bar: 'bar'
}
}
}
}
const p1 = new Person(),
p2 = new Person(true)
console.log(p1, p2) //Person{foo: 'foo'} {bar: 'bar'}
console.log(p1 instanceof Person) //true
console.log(p2 instanceof Person) //false
调用类构造函数必须使用new操作符,不能像普通函数一样可以不使用new调用
实例化后的实例,也含有constructor方法,所以也可以通过这个方法去new一个新的实例
class Person{}
const p1 = new Person()
console.log(p1.constructor)
const p2 = new p1.constructor() //需要用new调用
把类当成特殊函数
-
类就是一个特殊函数,可以通过
typeof操作符检测类型,检测出来是functionclass Person {} console.log(typeof Person) //function -
类标识符有
prototype属性,原型也有一个constructor属性指向类自身class Person {} console.log(Person.prototype) //{constructor: ƒ} console.log(Person === Person.prototype.constructor) //true -
可以通过
instanceof操作符检查构造函数原型是否存在于实例的原型链中class Person{} const p = new Person() console.log(p instanceof Person) //true -
虽然,在类的上下文中,类本身在使用的
new调用时就会被当成构造函数但是,类中定义的
constructor方法不会被当成构造函数,在对他使用instanceof操作符时会返回false而如果创建实例时直接将类构造函数当成普通构造函数来使用,那么
instanceof操作符的结果将会反转class Person {} const p1 = new Person() console.log(p1.constructor === Person) //true console.log(p1 instanceof Person) //true console.log(p1 instanceof Person.constructor) //false const p2 = new Person.constructor() console.log(p2.constructor === Person) //false console.log(p2 instanceof Person) //false console.log(p2 instanceof Person.constructor) //true -
类可以像其他对象和函数一样把类作为参数传递
const classList = [ class { constructor(id){ this.id_ = id console.log(`instance ${this.id_}`) } } ] function createInstance(classDefinition, id){ return new classDefinition(id) } const foo = createInstance(classList[0], '1212') //instance 1212 -
跟立即函数表达式相似,类也可以立即实例化
let p = new class Foo{ constructor(x){ console.log(x) //bar } }('bar') console.log(p); //Foo{}
实例、原型和类成员
实例成员
通过new操作符创建实例时,都会执行类构造函数constructor,在这个函数内部,可以为新创建的实例添加自有属性,每个实例对应一个唯一的成员对象,这意味着所有成员都不会在原型上共享
class Person {
constructor(){
this.name = new String('ZSW')
this.sayName = () => console.log(this.name)
this.nickName = ['XYZ', '小包']
}
}
let p1 = new Person()
let p2 = new Person()
p1.sayName()
p2.sayName()
console.log(p1.name === p2.name) //false
console.log(p1.sayName === p2.sayName) //false
console.log(p1.nickName === p2.nickName) //false
原型方法与访问器
由于方法一般都是希望共享的,所以我们可以在类块中定义方法作为原型方法
class Person {
constructor(){
this.locate = () => console.log('instance')
}
locate(){
console.log('prototype')
}
}
const p = new Person()
p.locate() //instance
Person.prototype.locate() //prototype
可以把方法定义在类块中,但是不能在类块中给原型添加原始值或对象作为成员数据
class Person{
name: 'ZXY'
}
//Uncaught SyntaxError: Unexpected identifier
类方法等同于对象属性,所以可以用字符串、符号、计算值作为键
const symbolKey = Symbol('symbolSey')
class Person {
constructor(){
this.locate = () => console.log('instance')
}
locate(){
console.log('prototype')
}
[symbolKey](){
console.log('Symbol属性')
}
['computed' + 'key'](){
console.log('计算属性')
}
}
const p = new Person()
p.locate() //instance
p[symbolKey]() //Symbol属性,此时在实例本身找不到该属性,就会去原型上面找
p.computedkey() //'计算属性
Person.prototype.locate() //prototype
类定义也支持获取和设置访问器
class Person {
set name(val){
return this.name_ = '笨' + val
}
get name(){
return this.name_
}
}
const p = new Person()
p.name = '小包'
console.log(p.name); //笨小包
静态类方法
可以在类上定义静态方法,这些方法通常用于执行不特定于实例的操作,也不要求存在类的实例
与原型成员类似,静态成员每个类上只能有一个
使用static关键字作为前缀,在静态成员中,this引用类自身,其他约定与原型成员一致
class Person{
constructor(){
this.locate = () => console.log('instance', this)
}
locate(){
console.log('prototype', this)
}
//定义在类本身上
static locate(){
console.log('class', this)
}
}
const p = new Person()
p.locate() //instance Person{}
Person.prototype.locate() //prototype {constructor: ...}
Person.locate() //class class Person{}
非函数原型和类成员
虽然类定义并不显示支持在原型或类上添加成员数据,但在类定义外部,可以手动添加
一般不会这么添加,因为在共享目标上添加可变数据成员是一种反模式,对象实例应该独自拥有通过this引用的数据
class Person{
sayName(){
console.log(`${Person.greeting} ${this.name}`)
}
}
Person.greeting = 'My Name is'
Person.prototype.name = 'XYZ'
const p = new Person()
p.sayName() //My name is XYZ
继承
虽然ES6新增特性中的类继承机制使用的是新语法,但是背后依旧是原型链
继承基础
ES6支持单继承
使用**extends关键字**,就可以继承任何拥有[[Constructor]]和原型的对象(不仅可以继承类,也可以继承普通构造函数)
派生类会通过原型链访问的类和原型上面定义的方法,this的值会反映调用相应方法的实例或者类
extend也可以在类表达式中使用,如let Bar = class extends Foo{}
class Vehicle {}
class Bus extends Vehicle{}
let b = new Bus()
console.log(b instanceof Bus) //true
console.log(b instanceof Vehicle) //true
构造函数、HomeObject、super()
派生类的方法可以通过super关键字引用他们的原型
这个关键字只能在派生类中用,而且仅限于类构造函数、实例方法和静态方法内部
class Bus extends Vehicle{
constructor(){
//不要在super之前引用this,否则会报错
super() //相当于super.constructor()
console.log(this instanceof Vehicle) //true
console.log(this) //Bus{hasEngine: true}
}
}
new Bus()
在静态方法中,通过super调用继承类上定义的静态方法
class Vehicle {
static identify(){
console.log('vehicle')
}
}
class Bus extends Vehicle{
static identify(){
super.identify()
}
}
Bus.identify() //vehicle
使用super所需要注意的问题:
super只能在派生类构造函数和静态方法中使用- 不能单独引用
super关键字,要么用它调用构造函数,要么用它引用静态方法 - 调用
super()会调用父类构造函数,并将返回的实例赋值为this super()的行为如同调用构造函数,如果需要给父类构造函数参数,则需要手动传入- 如果没有定义类构造函数,在实例化派生类时会调用
super(),而且会传入所有给派生类的参数 - 在类构造函数中,不能在调用
super()之前引用this - 如果在派生类中显式定义了构造函数,则要么必须在其中调用
super(),要么必须在其中返回一个对象
抽象基类
可能需要定义这样一个类,可供其他类继承,但是本身不会被实例化
虽然没有这种专门支持这种类的语法,但是通过new.target也可以实现
new.target可以返回new命令作用于的那个构造函数
class Vehicle {
constructor(){
console.log(new.target)
if(new.target === Vehicle){
throw new Error('Vehicle只能用于继承,不能实例化')
}
}
}
class Bus extends Vehicle{}
new Bus() //class Bus{}
new Vehicle() //Uncaught Error: Vehicle只能用于继承,不能实例化