ES6新引入class
关键字具有正式定义类的能力,表面上看起来支持正式的面向对象编程,实际上背后使用的仍然是原型和构造函数的概念。
一、类的定义
定义类的两种方式:类声明和类表达式。
// 类声明
class Person {}
// 类表达式
const Person = class {}
与函数定义不同的是,函数声明可以提升,类定义不能
console.log(ClassDeclaration) // ReferenceError: Cannot access 'ClassDeclaration' before initialization
class ClassDeclaration {}
console.log(ClassDeclaration) // [class ClassDeclaration]
另一个跟函数声明不同的是,函数受函数作用域限制,类受块作用域限制。
类的构成
类可以包含构造函数、实例方法、获取函数、设置函数和静态方法。类定义中的代码都在严格模式下进行。以下定义类的方式都有效:
// 空类定义
class Foo {}
// 有构造方法的类
class Bar {
constructor () {}
}
// 有获取函数的类
class Baz {
get myBaz() {}
}
// 有静态方法的类
class Qux {
static myQux() {}
}
二、类构造函数
当使用new操作符创建类的实例时,类中定义的constructor
函数会被调用。
class Person {
constructor () {
console.log('constructor invoked')
}
}
let per1 = new Person() // 'constructor invoked'
- 默认情况下,类构造函数会在执行之后返回this对象,该对象被用作实例化的对象。
- 类构造函数与普通构造函数的区别:调用类构造函数必须使用new操作符。 普通构造函数如果不使用new调用,会以全局的this(通常是window)作为内部对象。
- 把类当成特殊函数:通过
typeof
可以检测类标识符,表明它是一个函数:
console.log(typeof Person) // function
- 类标识符有
prototype
属性,而这个原型也有一个constructor
属性指向类自身。
console.log(Person === Person.prototype.constructor) // true
- 在类的上下文中,类本身在使用new调用时就会被当作构造函数,而类中定义的
constructor
方法不会被当作构造函数。但是如果在创建实例时将类构造函数当作普通构造函数使用,instanceof
结果会不一样:
let per1 = new Person()
console.log(per1 instanceof Person) // true
console.log(per1.constructor === Person) // true
console.log(per1 instanceof Person.constructor) // false
let per2 = new Person().constructor
console.log(per2 instanceof Person) // false
console.log(per2.constructor === Person) // false
console.log(per2 instanceof Person.constructor) // true
三、实例、原型和类成员
类中可以定义存在于实例上的成员,存在于原型上的成员,和存在于类本身的成员。
实例成员
每一个实例都对应一个唯一的成员对象,所有成员都不会在原型上共享:
class Person {
constructor () {
this.name = new String('Mike')
this.color = ['red','green']
}
}
let per1 = new Person()
let per2 = new Person()
console.log(per1.name === per2.name) // false
console.log(per1.color === per2.color) // false
原型方法与访问器
在类块中定义的所有内容都会定义在类的原型上
class Person {
sayHi () {
console.log('Hello')
}
}
Person.prototype.sayHi()
可以把方法定义在类构造函数中或者类块中,但不能给原型添加原始值或对象作为成员数据:
class Person {
name: 'mike' // 报错
}
类定义也支持获取和设置访问器:
class Person {
set name(newName) {
this.name_ = newName
}
get name() {
return this.name_
}
}
静态类方法
可以在类上定义静态方法,每个类中只能有一个静态成员。在类中使用static
关键字,在静态成员中,this
引用类自身。
class Person {
static sayHi(){
console.log('Hello')
}
}
Person.sayHi() // 'Hello'
四、继承
ES6中原生支持了类继承机制,但背后使用的依旧是原型链。使用extends
关键字,可以继承任何拥有[[Constructor]]
和原型的对象。不仅可以继承一个类,也能继承一个构造函数。
// 继承类
class Person {}
class Student extends Person {}
let stu1 = new Student()
console.log(stu1 instanceof Person) // true
console.log(stu1 instanceof Student) // true
// 继承普通构造函数
function Person () {}
class Student extends Person {}
let stu1 = new Student()
console.log(stu1 instanceof Person) // true
console.log(stu1 instanceof Student) // true
派生类都会通过原型链访问到类和原型上定义的方法,this的值会反映调用相应的方法的实例或者类:
class Person {
// 原型方法
goto(place) {
console.log(place, this)
}
// 类自身方法
static comeback(place) {
console.log(place, this)
}
}
class Student extends Person {}
let stu = new Student()
stu.goto('London') // London Student {}
Student.comeback('NewYork') // NewYork [class Student extends Person]
派生类的方法可以通过super
关键字引用它们的原型。关于super
的用法:
- 只能在派生类构造函数和静态方法中使用。
- 不能单独引用,要么用它调用构造函数,要么引用静态方法
class Person {}
class Student extends Person {
constructor () {
super // 报错
}
}
- 调用
super()
会调用父类构造函数,并将返回的实例赋值给this
class Person {}
class Student extends Person {
constructor () {
super()
console.log(this instanceof Person)
}
}
let stu = new Student() // true
super()
的行为如同调用构造函数,如果需要给父类构造函数传参,需手动传入- 如果没有定义类构造函数,在实例化派生类时会调用
super()
,而且会传入所有传给派生类的参数
class Person {
constructor (arg) {
console.log(arg)
}
}
class Student extends Person {}
let stu = new Student('Mike') // 'Mike'
- 在类构造函数中,不能在调用
super()
之前引入this
(原因可以联系前面说到的this
的值是由父类构造函数返回的实例,而父类函数的调用需要super()
)
class Person {}
class Student extends Person {
constructor () {
console.log(this) // 报错
}
}
let stu = new Student()
- 如果在派生类中显示定义了构造函数,则要么必须在其中调用
super()
,要么必须在其中返回一个对象
class Person {}
class Student extends Person {
constructor () {
super()
}
}
class Work extends Person {
constructor () {
return {}
}
}
抽象基类
抽象基类:可供其他类继承,但本身不会被实例化。通过new.target
来实现。new.target
保存通过new
关键字调用的类或函数。通过在实例化时检测new.target
是不是抽象基类,可以阻止对抽象基类的实例化:
class Person {
constructor () {
console.log(new.target)
if (new.target === Person){
throw new Error('Person cannot be directly instantiated')
}
}
}
class Student extends Person {}
new Student() // [class Student extends Person]
new Person() // [class Person]
// Person cannot be directly instantiated
通过在抽象基类构造函数中进行检查,可以要求派生类必须定义某个方法。因为原型方法在调用类构造函数之前就已经存在了,所以通过this
关键字来检查相应的方法。
class Person {
constructor () {
if (new.target === Person){
throw new Error('Person cannot be directly instantiated')
}
if (!this.foo){
throw new Error('Inheriting class must define foo()')
}
console.log('success!')
}
}
class Student extends Person {
foo () {
console.log('foo')
}
}
class Work extends Person { }
new Student() // success!
new Work() // Inheriting class must define foo()