ES6:class

163 阅读6分钟

一、class的概念和基本语法

class是ES6新出的一个语法糖,用来替代ES5的构造函数,它的绝大多数功能,使用ES5的语法都能做到。

ES5:

      function Animal(name) {
        this.name = name
      }
      Animal.play = function () {
        console.log('paly')
      }
      Animal.prototype.eat = function () {
        console.log(this.name + 'eat')
      }

ES6:

      class Animal {
        constructor(name) {
          this.name = name
        }
        eat() {
          console.log(this.name + 'eat')
        }
        static play() {
          console.log('play')
        }
      }

Animal类用Object.prototype.toString.call(Animal)打印结果为[object Function],说明class的本质还是函数,内部实现还是使用的ES5的构造函数。

二、类的实例化

1、类在调用时必须要结合new

ES5的函数在非严格模式下,调用时可以不加new,它就是一个普通函数;在严格模式下必须结合new调用。ES6的class默认就是开启了严格模式

      var cat = new Animal('喵喵')
      cat.eat()
      Animal.play()

2、类中的所有方法都是定义在类的prototype上

      class Animal {
        eat() {
          console.log('eat')
        }
      }

      const animal = new Animal()
      console.log(animal)

image.png

但是要注意,只有手动地往prototype上添加的属性/方法才是可枚举的。对于这一点,ES5都是手动添加到prototype上的,所以都可以枚举,ES6若是直接写在类中的方法,是不可枚举的

      class Animal {
        eat() {
          console.log('eat')
        }
      }
      Animal.prototype.sleep = function () {
        console.log('sleep')
      }
      console.log(Object.keys(Animal.prototype)) // ['sleep']

3、和ES5一样,实例的属性直接定义在this身上

ES5定义实例的属性:

      function Animal(name, age) {
        this.name = name
        this.age = age
      }

      var animal = new Animal('喵喵', 2)
      console.log(animal)

ES6定义实例的属性:

      class Animal {
        constructor(name, age) {
          this.name = name
          this.age = age
        }
      }

      const animal = new Animal('喵喵', 2)
      console.log(animal)

三、constructor方法

1、constructor是class中默认的方法,如果没写,会自动加上:

      class Animal {
        // constructor() {} // 这一句不写会自动加上
        eat() {
          console.log('eat')
        }
      }
      console.log(Object.getOwnPropertyNames(Animal.prototype)) // ['constructor', 'eat']

constructor中默认会return this,如果手动改变返回值,当return简单数据类型时会被忽略,当return复杂数据类型时会修改原来的return this。这一点和ES5构造函数的返回值是一致的

四、this的指向

      class Animal {
        getName(name = '旺财') {
          this.print(name)
        }
        print(text) {
          console.log(text)
        }
      }

      const animal = new Animal()
      animal.getName()
      const { getName } = animal
      getName() // Uncaught TypeError: Cannot read properties of undefined

类的方法中的this指向类的实例,但是将该方法提取出来,再进行调用时,此处getName中的this指向undefined

解决办法一:在构造函数中绑定getName的this,这样就不会找不到了

        constructor() {
          this.getName = this.getName.bind(this)
        }

解决办法二:使用箭头函数

        constructor() {
          this.getName = (name = 'xx') => {
            this.print(name)
          }
        }

相当于直接在类中写:

        getName = (name = 'xx') => {
          this.print(name)
        }

解决办法三:使用Proxy,获取方法的时候,自动绑定this

      function selfish(target) {
        const cache = new WeakMap()
        const handler = {
          get(target, key) {
            const value = Reflect.get(target, key)
            if (typeof value !== 'function') {
              return value
            }
            if (!cache.has(value)) {
              cache.set(value, value.bind(target))
            }
            return cache.get(value)
          }
        }
        const proxy = new Proxy(target, handler)
        return proxy
      }
      
      // 实例化
      const animal = selfish(new Animal())

五、class的继承

ES5的继承:(个人比较推崇圣杯模式继承,而不是通过Animal.apply(this, [...args]))

      function Animal(name) {
        this.name = name
      }
      Animal.prototype.eat = function () {
        console.log(this.name + 'eat')
      }
      function Cat(name, age) {
        Animal.call(this, name)
        this.age = age
      }
      inherit(Cat, Animal)

      var cat = new Cat('喵喵', 2)
      console.log(cat)
      cat.eat()

      // 圣杯模式继承
      function inherit(Target, Origin) {
        function Buffer() {}
        Buffer.prototype = Origin.prototype
        Target.prototype = new Buffer()
        Target.prototype.constructor = Target
        Target.prototype.super_class = Origin
      }

ES6:

在class中,通过extends关键字来实现继承,这比ES5修改原型链继承要清晰和方便许多。

      class Animal {
        constructor(name) {
          this.name = name
        }
        eat() {
          console.log(this.name + 'eat')
        }
      }

      class Cat extends Animal {
        constructor(name, age) {
          super(name)
          this.age = age
        }
      }

      const cat = new Cat('喵喵', 2)
      console.log(cat)
      cat.eat()

在Cat类中,constructor函数中必须要调用super,super表示父类的构造函数,用来创建父类的this对象。

子类必须要在constructor方法中调用super方法,否则新建实例时会报错。这是因为子类没有自己的this对象,而是继承父类的this对象,如果不调用super,子类就得不到this对象。

这和ES5的机制完全不同,ES6中子类中必须要先创建父类的this对象(调用super),然后在子类的构造函数中才能修改this。

子类中如果不写constructor,默认会加上constructor函数并调用super,就像这样:

        constructor(...args) {
          super(...args)
        }

六、类的prototype和__proto__

在ES5中,实例的__proto__等于其构造函数的prototype。class作为构造函数的语法糖,同时具有prototype和__proto__,因此存在两条继承链

  1. 子类的__proto__指向父类
  2. 子类的prototype的__proto__,指向父类的prototype
      class Animal {}
      class Cat extends Animal {}

      console.log(Cat.__proto__ === Animal) // true
      console.log(Cat.prototype.__proto__ === Animal.prototype) // true

实际上,extends等同于:

      Cat.__proto__ = Animal
      Cat.prototype.__proto__ = Animal.prototype

七、super

super可以当做函数使用,也可以当做对象使用。

第一种情况,super当做函数使用,代表父类的构造函数,ES6要求,子类的构造函数必须要执行一次super。当子类中不写构造函数时,系统会默认加上。

      class Animal {}
      class Cat extends Animal {
        constructor() {
          super() // super()代表父类Animal的构造函数。作为函数时,super()只能用在constructor中
        }
      }

super中的this:

      class Animal {
        constructor() {
          console.log(new.target.name) // Cat
        }
      }
      class Cat extends Animal {}
      new Cat()

第二种情况,当super当做对象使用时,指向父类的prototype:

      class Animal {
        getFood() {
          return '鱼'
        }
      }
      class Cat extends Animal {
        eat() {
          console.log(super.getFood()) // 相当于Animal.prototype.getFood()
        }
      }
      const cat = new Cat()
      cat.eat()

ES6规定,通过super调用父类的方法时,super会绑定子类的this。

      class A {
        constructor() {
          this.x = 1
        }
        print() {
          console.log(this.x)
        }
      }

      class B extends A {
        constructor() {
          super()
          this.x = 2
        }
        m() {
          super.print()
        }
      }

      let b = new B()
      b.m() // 2

super.print()实际上是super.print.call(this)

由于super绑定了子类的this,通过super对属性赋值实际上就是通过this对属性赋值:

      class A {
        constructor() {
          this.x = 1
        }
      }

      class B extends A {
        constructor() {
          super()
          this.x = 2
          super.x = 3
          console.log(super.x) // undefined
          console.log(this.x) // 3
        }
      }

      let b = new B()

super.x = 3实际上是this.x = 3,而super.x实际上是A.prototype.x

另外,在使用super时,必须明确super是作为函数还是对象使用,函数就要加括号,对象就要跟着属性或方法,直接打印super会报错

八、class的静态方法

在方法名前加上static关键字,表示这个方法是类的静态方法,只能由类来调用

      class Animal {
        static sleep() {
          console.log('sleep')
        }
      }

      Animal.sleep()

静态方法可以被子类继承:

      class Animal {
        static sleep() {
          console.log('sleep')
        }
      }

      class Cat extends Animal {}
      Cat.sleep()

静态方法也可以被super直接调用:

      class Cat extends Animal {
        static catSleep() {
          console.log(super.sleep())
        }
      }
      Cat.catSleep()

九、class的静态属性和实例属性

ES6明确规定,在类中只有静态方法,没有静态属性,所以ES7之前只能通过这种方式定义静态属性:

      class Animal {}
      Animal.height = 20

在ES7的提案中,weight = 5是在constructor中this.weight = 5的简写

      class Animal {
        weight = 5 // 实例属性
        static age = 2 // 静态属性
      }

十、new.target

new.target是ES6对new操作符的一个补充属性,必须在函数内使用,如果这个函数是被new作用的,那么new.target返回这个函数。如果这个函数不是通过new调用的,那么new.target返回undefined

      function Animal() {
        console.log(new.target)
      }

      Animal() // undefined
      new Animal() // f Animal(){}

利用new.target的这个特点,可以让构造函数必须使用new来调用:

      function Animal(name) {
        if (new.target === undefined) throw new Error('必须要使用new生成实例') // 只需加上这句判断
        this.name = name
      }

new了谁,new.target指向谁:

      class Animal {
        constructor() {
          console.log(new.target)
        }
      }
      class Cat extends Animal {}
      new Animal() // Animal类
      new Cat() // Cat类

利用这个特点,可以设置一个类不能独立使用,必须继承后才能使用:

      class Animal {
        constructor() {
          if (new.target === Animal) throw new Error('Animal不可以被实例化')
        }
      }
      class Cat extends Animal {}
      // new Animal() // 报错
      new Cat() // Cat类

十一、get和set

      class Animal {
        name = 'xx'
        get name() {
          return this.name
        }
        set name(val) {
          this.name = val
        }
      }

      const animal = new Animal()

      console.log(animal.name) // xx
      animal.name = 'yy'
      console.log(animal.name) // yy