美文网首页
类(Javascript)

类(Javascript)

作者: 东方三篇 | 来源:发表于2021-11-29 11:22 被阅读0次

    类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 调用类型的构造函数会执行如下操作:

    1. 在内存中创建一个新对象。

    2. 新对象内部的[[Prototype]]指针被复制为构造函数的prototype属性。

    3. 构造函数内部的this被赋值为这个新对象(this指向新对象)。

    4. 执行构造函数内部代码。

    5. 如果构造函数返回非空对象, 则返回该对象; 否则, 刚创建的新对象。

      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)已经抛弃混入模式, 转向了复合模式(把方法提取到独立的类和辅助对象中,然后把他们组合起来,但不适用继承)**
    

    相关文章

      网友评论

          本文标题:类(Javascript)

          本文链接:https://www.haomeiwen.com/subject/weakxrtx.html