javascript 高级程序设计 8.4 类

53 阅读6分钟

类的出现,是因为通过函数实现继承,会导致代码冗长和混乱.类是ES6的新语法糖,表面上支持了面向对象编程,实际上内部还是原型和构造函数的概念.

1.类定义

声明式:

class Person{}

类表达式

const Animal = class {}

*注意类表达式不可以变量提升,如果在类表达式未定义之前对他进行调用,就会直接异常

*类受到块级作用域限制,函数是函数作用域

类的构成

类中可以包含: 构造函数方法,实例方法,获取方法,设置方法,静态类方法.这些方法都是非必需的.

类表达式的名称是可选的,可以在外面通过类表达式.name取到类的名字,但是无法直接去调用这个类的名字.

2.类构造函数

通过new一个类创建新实例的时候,会调用constructor函数,而如果没有定义构造函数,默认构造函数就是空函数.

实例化

通过new去实例化一个类的时候,就是调用了他的构造函数

使用new代表了: 

1.先分配一块内存空间

2.将实例的内部指针指向构造函数的原型

3.将构造函数的this指向实例

4.执行构造函数中的内容

5.如果构造函数里返回了非空对象,就返回该对象,否则就返回刚创建的对象.

*第五步里,如果不手动返回对象,就会把this对象返回,而如果手动返回的不是this对象,而是其他对象,那么这个返回的实例无法通过instanceOf去监测和类的关系

*实例化的时候如果传入了参数,会作为构造函数的参数

之前也使用过普通的构造函数,普通的构造函数如果不使用new操作符,里面的this就是全局的window,而类构造函数如果不使用new,就会报错.类构造函数并不特殊,在实例化之后就是实例的普通方法,只不过还是得通过new去调用.

把类当作特殊函数

js中没有正式的类这个类型,其实类就是一种特殊函数,依据有三:

1. typeof 类名 为function

2.类具有prototype属性,并且prototype属性里也有constructor指回prototype

3.能够通过instanceOf检查类的实例是否存在于原型链中

类中定义的constructor函数不被当作构造函数,实例在对类名.constructor使用instanceOf的时候返回false。但是,如果在创建实例的时候使用的就是new 类名.constructor,那么结果就正相反。

类是js的一等公民,因此可以像其他对象或函数一样作为参数进行传递。

类可以和函数一样在任何地方被定义,比如数组中。

let classList = [
    class {
        constructor(id) {
            this.id = id
            console.log(`instance${this.id}`)
        }
    }
]
function createInstance(classItem, id) {
    return new classItem(id)
}
let p1 = createInstance(classList[0],666) // instance666

和立即调用函数类似,类可以立即实例化

let p = new class Foo {
    constructor(id) {
        console.log(id)
    }
}('666')  // 666

3.实例,原型,类成员

类的语法可以比较方便的定义应该存在于实例的成员,原型的成员,和类本身上的成员

1.实例成员

每次调用new操作符,都会去执行构造函数,构造函数内部,可以为这个实例(this)设置自有属性,设置的值是没有什么限制的,生成的实例也是彼此独立,互不影响,不会在原型上相互影响.

2.原型方法和访问器

为了在实例间共享方法,类语法把在类块里定义的方法作为原型方法.

class Person {
    constructor() {
        this.locate = () => {console.log('实例方法')}
    }
    locate() {
        console.log('原型方法')
    }
}
let p = new Person()
p.locate() // '实例方法'
p.prototype.locate() // '原型方法'

但是注意不能在类块里添加原始值或者对象

class Person {
    name: 'jake'
}
// 这么写会报错

类方法等同于对象属性,所以可以用字符串,符号,或者计算值作为键

类定义支持设置器和访问器

class Person {
    set name(newName) {
        this.name_ = newName
    }
    get name() {
        return this.name_
    }
}
let p = Person()
p.name = 'jake'
console.log(p.name) // 'jake'

3.静态方法类

静态类方法不执行特定于某个实例的方法,它是类的方法,并不会要求有实例.而且每个类里的静态方法不能重名.通过static关键字作为前缀,适合做实例工厂

class Person {
    constructor(age) {
        // 定义在实例上
        this.age_ = age    
    }
    // 定义在原型上
    sayAge() {
        console.log(this.age_)
    }
    // 使用随机年龄创建并返回Person对象
    // 定义在类本身上
    static create() {
        return new Person(Math.floor(Math.random()*100))
    }
}
console.log(Person.create) // 打印了Person的随机实例

4.非函数类型和类成员

虽然没办法在类块和原型里直接添加成员数据,但是在外面可以

class Person {
    constructor() {
        console.log(`${this.greeting}${this.name}`)
    }
}
Person.greeting = 'my name is' // 在类上定义数据成员
Person.prototype.name = 'jake' // 在原型上定义数据成员
let p = new Person() // 'my name is jake'

5.迭代器和生成器方法

 类语法可以在类本身和原型里去定义生成器方法

Person {
    // 定义原型上的生成器函数
    *createPrototypeIterator() {
        yield 'jack';   
        yield 'mars';
        yield 'jake'; 
    }
    // 定义类本身的生成器函数
    static *createStaticIterator() {
        yield 'zhangsan',
        yield 'lisi',
        yield 'wangwu'
    }
}
let p1 = Person.createStaticIterator()
console.log(p1.next().value) // 'zhangsan'
console.log(p1.next().value) // 'lisi'
console.log(p1.next().value) // 'wangwu'
let p = new Person()
let p2 = p.createPrototypeIterator()
console.log(p2.next().value) // 'jack'
console.log(p2.next().value) // 'mars'
console.log(p2.next().value) // 'jake'

继承

Es6新特性里添加了类的继承,虽然使用的是新的语法,但是实际上背后使用的还是原型链.

1.基础继承

es6的类支持单继承,可以继承类和构造函数,通过extends

// 继承类
class Person {}
class Chinese extends Person{}
let zhangsan = new Chinese()
console.log(zhangsan instanceOf Chinese) // true
console.log(zhangsan instanceOf Person) // true
// 继承构造函数
function Animal() {}
class Cat extends Animal{}
let mimi = new Cat()
console.log(mimi instanceOf Cat) // true
console.log(mimi instanceOf Animal) // true

2.构造函数,super(),HomeObject

super()可以引用原型,但是必须在类构造函数/实例方法/静态方法

在类构造函数里使用:

class Father {
  constructor() {
    this.name = '张三'
  }
}
class Son extends Father {
  constructor() {
    super()
    console.log(this instanceof Father)
    console.log(this)
  }
}
new Son()
// true
// Son{name:'张三'}
// Son{name:'张三'}

在静态方法里,可以用super调用继承的父类的静态方法

class Father {
    static look() {
        console.log('father look')    
    }
}
class Son extends Father {
    static see() {
        super.look()    
    }
}
Son.see() // 'father look'

HomeObject

这个是个隐藏属性,能确保使用super的时候不会发生继承和调用错误,和super绑定,是个自动指针,简单理解就是指向父类.

关于super,需要注意下面几点:

  1. 只能在派生类构造函数和静态方法里使用
  2. 不能单独引用super关键字,要么super()调用构造函数,要么super.静态方法调用静态方法
  3. super()会调用父类构造函数并且把返回的实例赋值给this
  4. super()形同调用构造函数,如果要给父类构造函数传参得手动传入
  5. 如果派生类没有定义类构造函数,就会默认的调用super()
  6. 类构造函数中,不能在super()前使用this
  7. 如果派生类里定义了构造函数,就必须在里面调用super()或者返回一个对象

3.抽象基类

有时候我们希望一个类只能被继承而不能被实例化,就可以定义抽象基类.

抽象基类是通过new.target实现的,new.target能保存通过new操作符调用的类或者函数,可以在实例化的时候进行检测,判断这个类是不是抽象基类,是的话就报错就行了

class Father {
    constructor() {
        console.log(new.target)
        if(new.target === Father) {
            throw new Error('Father是抽象基类,不可实例化!')    
        }
    }
}
class Son extends Father {}
new Son() // 正常
new Father() // 'Father是抽象基类,不可实例化!'

可以在抽象基类的构造函数中进行检查,让子类必须定义某个方法,因为原型方法在调用类构造函数之前就已经存在,所以可以用this进行检测

calss Father {
    constructor() {
        if(new.target === Father) {
            throw new Error('Father是抽象基类,不可实例化!')        
        }
        if(!this.look){
            throw new Error('子类必须具有方法:look')
        }
        console.log('success!')
    }
}
class Son1 extends Father{
    look() {}
}
class Son2 extends Father{}
new Son1() // succsee
new Son2() // Error: 子类必须具有方法:look

4.继承内置类型

ES6内置了很多常用的类型,方便扩展

class FatherArray extends Array{
    xipai() {
        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]]
        }
    }
}
let arr = new FatherArray(1,2,3,4,5)
console.log(arr instanceOf Array) // true
console.log(arr instanceOf FatherArray) // true
console.log(arr) // [1,2,3,4,5]
arr.xipai()
console.log(arr) // [3,1,4,5,2]

内置类型有时候会返回新的实例,但是默认情况下返回的实例和原始实例的类型是一致的,所以不用担心.如果想要覆盖这个默认行为,可以覆盖Symbol.species访问器,这个访问器决定在创建返回的实例时使用的类.

5.类混入

不推荐使用,暂时先放着