类class(Javascript)
为了解决js混合和冗长的问题, ES6新引入了 class关键字具有正式定义类的能力。class只是ES中新的基础性语法糖结构。虽然看上去 ES6的 class 正式支持了面相对象编程, 但实际上背后使用依然是原型和构造函数的概念。
1. 类定义
与函数声明类似,都是两种: 类声明 和 类表达式
class Person {} // 类声明式
const Person = class { // 类表达式
constructor () {}
get myBaz () {}
static myQux () {}
}
/**
* 类表达式在它们被求值前不能引用,和函数一样
* 类声明式不会变量提升,函数声明式会变量提升
* 类受块作用域限制,函数受函数作用域限制
* 类可以包含: 构造函数方法, 实例方法, 获取函数, 设置函数和静态类方法, 但这写都不是必然的。
*/
2. 类构造函数 constructor
constructor 关键字用于在类定义块内部创建类的构造函数。方法名 constructor 会告诉解释器在使用 new 操作符创建类的实例时,应该调用这个函数。 构造函数的定义不是必须的,不定义相当于默认定义的一个空函数。
1. 实例化
使用new 操作符,实例化 Person 的操作等于使用 new 调用其构造函数。使用 new 调用类型的构造函数会执行如下操作:
-
在内存中创建一个新对象。
-
新对象内部的[[Prototype]]指针被复制为构造函数的prototype属性。
-
构造函数内部的this被赋值为这个新对象(this指向新对象)。
-
执行构造函数内部代码。
-
如果构造函数返回非空对象, 则返回该对象; 否则, 刚创建的新对象。
const Animal = class {} const Person = class { constructor () { console.log('person constructor') } } const Vegetable = class { constructor () { this.color = 'red' } } const a = new Animal() const p = new Person() // person constructor const v = new Vegetable() console.log(v.color) // red const Person2 = class { constructor (name) { // 传入的参数作为 类构造函数的参数传入 this.name = name || null } } const p2 = new Person2('Tom') console.log(p2.name) // Tom const p3 = new Person2 // 如果不传入参数 类后面的括号可以省略 console.log(p3.name) // null /** * 默认情况下, 类构造函数会在执行之后返回this对象。 * 构造函数返回的对象会被用作实例化的对象,如果没有什么引用新创建的this对象,则这个对象会被销毁。 * 如果构造函数返回的不是this对象, 则这个对象不能通过instanceof检测出跟类有关联,这个对象的原型指针没有被修改 */ class Person { constructor (override) { this.foo = 'foo' if (override) { return { bar: 'bar' } } } } const p1 = new Person() const p2 = new Person(true) console.log(p1) // Person {foo: 'foo'} console.log(p1 instanceof Person) // true console.log(p2) // {bar: 'bar'} console.log(p2 instanceof Person) // false
类构造函数与普通构造函数的区别是, 调用类构造函数必须使用 new 操作符, 如果不用就会报错。而调用普通构造函数时,使用 new 就是构造函数,不使用 new 就相当于调用普通函数。
2. 把类当做特殊函数
ES中没有正式的类这个类型。实际上ES中的类就是一种函数。使用 typeof 能检测到 类 是函数类型function
// 类标签也有一个prototype属性,这个原型也有一个constructor属性指向类自身,这点与普通构造函数一致
class Person {}
console.log(Person.prototype) // { constructor: f () }
console.log(Person === Person.prototype.constructor) // true
// 与立即调用函数表达式相似,类也可以立即实例化
const p = new class Foo { // 因为是类表达式, 所有类名是可选的
constructor (x) {
this.x = x || null
}
}('bar')
console.log(p.x) // bar
3. 实例,原型和类成员
类的语法可以非常方便的定义应该存在于实例上的成员, 应该存在于原型上成员,应该存在于类本身的成员。
1. 实例成员
每次通过 new 调用类标识符时, 都会执行类构造函数。 在这个函数内部,可以为新创建的实例(this)添加“自有”属性。在构造函数执行完毕后, 仍然可以给实例添加新成员。 每个实例都对应唯一的成员对象, 意味着所有成员都不会在原型上共享。
class Person {
constructor () {
this.name = new String('Jack')
this.sayName = () => console.log(this.name)
this.nickNames = ['Jake', 'J-Dog']
}
}
const p1 = new Person()
const p2 = new Person()
p1.sayName() // [String: 'Jack']
p2.sayName() // [String: 'Jack']
console.log(p1.name === p2.name) // false 包装类型
console.log(p1.nickNames === p2.nickNames) // false
console.log(p1.sayName === p2.sayName) // false
p1.name = p1.nickNames[0]
p2.name = p2.nickNames[1]
p1.sayName() // Jake
p2.sayName() // J-Dog
2. 原型方法与访问器
为了在实例间共享方法, 类定义语法把类块中定义的方法作为原型方法。
class Person {
constructor () {
// 添加到this上的所有内容都会存在于不同的实例上
this.locate = () => console.log('instance')
}
// 在类块中定义的所有内容都会定义在类的原型上
locate () {
console.log('prototype')
}
proLocate () {
console.log('proLocate')
}
}
const p = new Person()
p.locate() // instance
p.proLocate() // proLocate
Person.prototype.locate() // prototype
// 不能在类块中给原型添加原始数值或对象作为成员数据
// class Person {
// name: 'Jack'
// }
// Uncaught SyntaxError: Unexpected token
// 类定义 支持 获取和设置访问器,语法与普通函数一致
class Person {
set name (newName) {
this.name_ = newName
}
get name () {
return this.name_
}
}
const p = new Person
p.name = 'Jack'
console.log(p.name) // Jack
3. 静态类方法
可以在类上定义静态方法。 这些方法通常用于执行不特定于实例的操作, 也不要求存在类的实例。与原型成员类似,每个类上只能有一个静态成员。静态类成员在类定义重工使用 static 关键字作为前缀。 在静态成员中, this引用类自身。其他所有约定与原型成一样。
class Person {
constructor () {
// 添加到替换ISD所有内容凑会存在于不同的实例上
this.locate = () => console.log('instance', this)
}
// 定义在类原型对象上
loacte () {
console.log('prototype', this)
}
// 定义在类本身上
static locate () {
console.log('class', this)
}
}
const p = new Person
p.loacte() // instance, Person {}
Person.prototype.locate() // prototype, {constructor: ...}
Person.locate() // class, class Person {...}
// 静态方法非常适合作为实例工厂
class Person {
constructor (age) {
this.age_ = age
}
sayAge () {
console.log(this.age_)
}
static create () {
// 使用随机年龄返回一个Person实例
return new Person(Math.floor(Math.random() * 100))
}
}
console.log(Person.create()) // Person { age_: 89 }
4. 非函数原型和类成员
虽然类定义不显式支持在原型或类上添加成员数据, 但在类定义外部可以手动添加。
class Person {
sayName () {
console.log(Person.greeting, this.name)
}
}
// 在类上定义数据成员
Person.greeting = 'my name is'
// 在原型上定义数据成员
Person.prototype.name = 'Jack'
const p = new Person
p.sayName() // my name is Jack
5. 迭代器与生成器方法
类定义语法支持在原型和类本身上定义生成器方法
class Person {
// 在原型上定义生成器方法
*createNickNameIterator () {
yield 'Jack'
yield 'Jake'
yield 'J-Dog'
}
// 在类上定义生成器方法
static *createJobIterator () {
yield 'Butcher'
yield 'Baker'
yield 'Candlestick maker'
}
}
const jobIter = Person.createJobIterator()
console.log(jobIter.next().value) // Butcher
console.log(jobIter.next().value) // Baker
console.log(jobIter.next().value) // Candlestick maker
const p = new Person
const nickIter = p.createNickNameIterator()
console.log(nickIter.next().value) // Jack
console.log(nickIter.next().value) // Jake
console.log(nickIter.next().value) // J-Dog
// 所以可以通过添加一个默认的迭代器,把类变成可迭代的对象
class Person {
constructor () {
this.nicknames = ['Jack', 'Jake', 'J-Dog']
}
*[Symbol.iterator] () {
yield *this.nicknames.entries()
}
}
const p = new Person
for(let [idx, nickname] of p) {
console.log(nickname)
}
4. 继承
ES6虽然类继承使用的新语法,但背后依然使用的是原型链。
1. 继承基础
ES6支持单继承。使用 extends 关键字, 就可以继承任何拥有[[Contruct]]和原型的对象。这就意味着不仅可以继承类,也可以继承普通的构造函数。
class Vehicle {}
// 继承类
class Bus extends Vehicle {}
const b = new Bus
console.log(b instanceof Bus) // true
console.log(b instanceof Vehicle) // true
// 继承构造函数
const Person = function () {}
class Engineer extends Person {}
const e = new Engineer
console.log(e instanceof Engineer) // true
console.log(e instanceof Person) // true
/**
* 类和原型上定义的方法都会带到派生类。 this的值会反映调用相应方法的实例或者类
*/
class Vehicle {
indentifyPrototype (id) {
console.log(id, this)
}
static indentifyClass (id) {
console.log(id, this)
}
}
const Bus = class extends Vehicle {}
const v = new Vehicle
const b = new Bus
v.indentifyPrototype('vehicle') // vehicle Vehicle {}
b.indentifyPrototype('bus') // bus Bus {}
Vehicle.indentifyClass('vehicle') // vehicle [class Vehicle]
Bus.indentifyClass('bus') // bus [class Bus extends Vehicle]
2. 构造函数、HOmeObject 和 super
派生类的方法可以通过从super 关键字引用他们的原型。这个关键字只能在派生类中使用, 而且仅限于构造函数、实例方法和静态方法内部。在类构造函数中 super 可以调用父类构造函数。
class Vehicle {
constructor () {
this.hasEngine = true
}
}
class Bus extends Vehicle {
constructor () {
// !!! 不要在调用 super 之前引用 this. 否则会抛出 ReferenceError
super() // 相当于 super.constructor()
console.log(this instanceof Vehicle) // true
console.log(this) // Bus {hasEngine: true}
}
}
new Bus()
// 在静态方法中通过 super 调用继承的类上定义的静态方法
class Vehicle {
static indentify () {
console.log('vehicle')
}
}
class Bus extends Vehicle {
static indentify () {
super.indentify()
}
}
Bus.indentify() // vehicle
super使用时注意的问题:
-
super 只能在派生类构造函数和类的静态方法中使用。
-
不能单独引用super关键字,要么它调用构造函数,要么它引用静态方法。
-
调用 super() 会调用父类构造函数, 并将返回的实例赋值给this。
class Vehicle {}
class Bus extends Vehicle {
constructor () {
super()
console.log(this instanceof Vehicle)
}
}
new Bus() // true
- super 的行为如同调用构造函数, 如果需要给父类构造函数传入参数, 则需要手动传入。
class Vehicle {
constructor (name) {
this.name = name
}
}
class Bus extends Vehicle {
constructor (name) {
super(name)
}
}
const b = new Bus('Tom')
console.log(b.name) // Tom
- 如果没有定义类构造函数, 在实例化派生类时会调用 super() , 而且会传入所有传给派生类的参数。
class Vehicle {
constructor (name) {
this.name = name
}
}
/**
* 在定义派生类 Bus 时,没有显式的定义派生类的 constructor
* 这时这实例化 派生类 Bus 时,会自动调用 super()
* 而且会把传入 派生类 Bus 的所有参数,都传入 父类的类构造函数中
*/
class Bus extends Vehicle {}
const b = new Bus('Tom')
console.log(b.name) // Tom
-
在类构造函数中, 不能在 super() 之前调用 this。
-
如果在派生类中显式的定义了构造函数, 则要么必须在其中调用super(), 要么必须在其中返回一个对象。
class Vehicle {}
class Bus extends Vehicle {
// 没有在派生类中显式定义 constructor
}
class Van extends Vehicle {
// 在派生类中显式定义 constructor
constructor () {
super() // 要么 super()
return {} // 要么 返回一个对象
}
}
3. 抽象基类
有时候需要定义一个这样的类, 用来供派生类继承,但自己本身不会被实例化。 可以通过 **new.target**来实现, new.target 保存通过 new 关键字调用的类或函数。通过在实例化时检测 new.target是不是抽象基类, 可以阻止抽象基类的实例化。
// 抽象基类 定义 普通类定义无区别,只是使用 new.targe 来判断能否实例化而已
class Vehicle {
constructor () {
if (new.target === Vehicle) {
throw new Error('Vehicle是抽象基类不能实例化!')
}
}
if (!this.foo) {
throw new Error('想要继承Vehicle,必须要定义 foo 方法!')
}
}
// 派生类
class Bus extends Vehicle {
foo () {}
}
class Van extends Vehicle {}
new Bus() // class Bus
new Vehicle() // Error: Vehicle是抽象基类不能实例化!
new Van() // Error: 想要继承Vehicle,必须要定义 foo 方法!
4. 继承内置类型
5. 类混入
把不同类行为集中到一个类是一种常见的js模式。 可以通过 Object.assgin()方法来实现。
**很多javascript框架(特别是react)已经抛弃混入模式, 转向了复合模式(把方法提取到独立的类和辅助对象中,然后把他们组合起来,但不适用继承)**
网友评论