Class类与继承(JS高程阅读随笔)

158 阅读6分钟

接上篇 ES5对象创建与继承

搞清楚ES5是怎么创建对象与继承的之后,接下来看看,ES6的对象创建与继承。

开头先定个性,ES6的类(也就是class),其实是ES5创建对象的一种语法糖,本质上还是基于原型的对象创建形式以及继承方式。

再说透一点,class的继承其实就是寄生组合式继承

类定义

ES6中类有2中定义方式

  • 类声明
  • 类表达式
// 类声明
class A {}
// 类表达式
var a = class A {} // A可写可不写

需要注意的是,和函数声明不同的是,类声明不能提升,在声明之前调用,会报错。

类的构成

  • 构造函数方法
  • 实例方法?(是实例方法还是原型方法?)
  • get、set
  • 静态类方法

所有以上部分,都不是必须的,空的类也能声明。

name属性

name 可以访问到 类名称,内部可以通过 类标识符[name]访问 以及 变量[name]访问,外部只能通过 变量[name]访问,见代码:

var Person = class PersonName{
	identiify () {
		console.log(Person.name, PersonName.name)
	}
}

let p = new Person()

p.identiify() // PersonName PersonName

console.log(Person.name) // PersonName
console.log(PersonName.name) // 报错

类构造函数 constructor

类的构造函数方法 constructor ,可以理解成es5中的构造函数,在new关键字调用的时候执行。

constructor不是必须写的,如果不写,会自动补全一个空函数给constructor。

实例化

通过new关键字调用类的时候,相当于调用constructor进行实例化,步骤与ES5的new关键字调用步骤是一样的,简单回顾如下:

  • 先创建一个空对象O
  • 将对象O上的 [[prototyp]] 属性,指向构造函数的 prototype对象
  • 在O之上调用constructor,将构造函数中绑定在this上的属性绑定到对象O上(可以理解为constructor.call(O))
  • 返回对象O

new ClassName() 是否需要这个括号?

可写可不写。当需要constructor传参的时候需要,不需要传参的时候可以不写。

var Person = class PersonName {
	constructor (name) {
		this.name = name
	}
}

let p1 = new Person // 可以创建
let p2 = new Person('Kobe') // 也可以创建

constructor执行后返回的是什么?

当constructor 有显式return返回值时,那么new接收到的就是constructor的返回值。没有return时,默认返回this。

这里要注意,如果显式了return了值,那么实例与class之间,instanceOf的链接就断了。

class Person {
	constructor (override) {
		if (override) {
			return { name: 'new instance'}
		}
	}
}

let p1 = new Person
let p2 = new Person(true)

console.log(p1 instanceof Person) // true
console.log(p2 instanceof Person) // false

类构造函数能不能像普通构造函数一样不用new调用?

不行。会报错。

“类”的数据类型是什么?

是函数。

到目前为止,JS中还没有正式的class类型,class的类型实际上是function。

class Person {}
typeof Person // function

实例属性、原型属性 和 其他类成员

实例属性

这里实例属性,泛指属性以及方法。

想要给 class 添加实例属性,只要将属性在constructor方法内部绑定到其this上即可。

通过new关键字调用类构造函数,会将类构造函数中this上的属性全部赋值给新创建的实例,且不会在各实例之间共享。

在实例化之后,也可以为新的实例添加实例属性或者修改已有实例属性。

class Person {
	constructor () {
		this.name = 'Bob'
		this.nickName = ['Will', 'Jean']
	}
}

let p1 = new Person()
let p2 = new Person()

console.log(p1.name) // Bob
console.log(p2.name) // Bob
console.log(p1.name === p2.name) // false

p1.name = p1.nickname[0]
p2.name = p2.nickname[1]

console.log(p1.name) // Will
console.log(p2.name) // Jean

多说一句,对比ES5的继承,实际上,属性是更适合定义在constructor上的,但方法不适合。原因再简单回顾一下:

原型上定义引用类型的属性,会被实例共享;

构造函数内定义方法,会有代码复用的问题。

原型方法与访问器

一句话,在类块中定义的方法,就是原型方法。

class Person {
	constructor () {
		this.name = 'Bob'
		this.sayName = function () { // 定义在实例的方法
			console.log(this.name + ' in instance')
		}
	}

	sayName () {
		console.log(this.name + ' in prototype') // 定义在原型的方法
	}
}

类块中能不能定义原始值类型或对象类型属性?

不可以,会报错。类块中,只能定义方法。

class Person {
	name: 'Bob' // 不合法,会报错
}

这里提一句,“实例属性的新写法”,简单说就是,在类块的最顶部,用赋值表达式,可以为实例先定义一批属性,相比于写在constructor里面的好处是,一上来就能看到所有的实例属性。(个人建议不要这么做,简化操作,避免出错

class Person {
	name = 'Bob'
	age = 20
	constructor () {}
}

getter和setter

类中支持定义属性的getter和setter,写法是直接将get、set方法写在类块中。

class Person {
  get name () {
    return this.name_ // 这里为什么是name_?
  }
  set name (newName) {
    this.name_ = newName // 这里为什么是name_?
  }
}
let p = new Person();
p.name = 'Jake';
console.log(p.name); // Jake

上面代码,为什么get和set中,赋值的属性是this.name_而不是this.name?

这里其实是创建了一个新的私有属性,来代替name,因为如果在set内部调用this.name,相当于在递归调用set,最终结果就是“栈溢出”。

静态类方法

什么是静态类方法?

只能通过类本身调用,且不会被实例继承的方法。注意,这里说的是“方法”。

用关键字 static,在类块内部创建,或者,直接给类添加也可以。

class Person {
  static staticFun () { console.log('im a static function') } // 在类块内部创建静态类方法
}

// 在外部创建静态类方法
Person.staticFun2 = function () { console.log('im another static function') }

let p1 = new Person()

p1.staticFun() // 报错,TypeError: staticFun is not a function
p1.stati2cFun() // 报错,TypeError: staticFun2 is not a function

Person.staticFun() // im a static function
Person.staticFun2() // im another static function

有没有静态类属性?

有的。但是和静态类方法的定义方式不同,静态类属性只能再类块外部绑定。

class Person {}
Person.staticName = 'staticName' // 目前只能通过这样的方式创建静态类属性

let p1 = new Person()

console.log(p1.staticName) // undefined

原理上,静态属性应该是不能被绑定到实例的this以及构造函数的this上,所以无法被实例继承,可以看下面的代码示例:

class Person {
  sayName1 () {
    console.log(Person.staticName)
  }
	sayName2 () {
    console.log(this.staticName)
  }
}
Person.staticName = 'staticName' // 目前只能通过这样的方式创建静态类属性

let p1 = new Person()

p1.sayName1() // staticName
p1.sayName2() // undefined

类继承

先明确一点,ES6的class继承,背后一样也是基于ES5的原型链继承,或者更直接一点,就是“寄生组合式继承”。

继承基础

ES6中,使用extends关键字实现继承,既可以继承class类,也可以继承构造函数。

// 继承类
class Vehicle {}
class Bus extends Vehicle {}

// 继承构造函数
function Person () {}
class Child extends Person {}

注意静态属性和方法也是可以被继承的。

class Vehicle {
	static staticFun () {console.log('im static function')}
}
Vehicle.staticName = 'staticName'

class Bus extends Vehicle {}

Bus.staticFun() // im static function
console.log(Bus.staticName) // staticName

super方法

super是干什么用的?

super是class继承语法中,用来让子类能够继承父类的实例方法或属性的方法。相当于调用了 父类的构造函数。

super只能在子类的构造函数或者静态方法中使用。

class Vehicle {
	constructor () {
		this.hasEngine = true
	}
}

class Bus extends Veicle {
	constructor () {
		super()
	}
}

在super调用之前,不能在constructor中引用this,否则会报错

class Vehicle {
	constructor () {
		this.hasEngine = true
	}
}

class Bus extends Veicle {
	constructor () {
		this.name = 'bus' // 在实例化Bus的时候报错
		super()
	}
}

let bus = new Bus() // 报错

super能不能不写?

如果在子类中有显式声明类构造函数,那super必须写,或者让子类的constructor return一个对象。

class Vehicle {}
class Bus extends Veicle {
	constructor () {}
}
let bus = new Bus() // 会报错

class Person {}
class Doctor extends Person {
	constructor () {
		return {}
	}
}
let doc = new Doctor() // 没问题

super可以用来给父类构造函数传参

class Vehicle {
	constructor (hasEngine) {
		this.hasEngine = hasEngine
	}
}
class Bus extends Vehicle {
	constructor () {
		super(true)
	}
}
let bus = new Bus()

console.log(bus.hasEngine) // true