JavaScript面向对象—深入ES6的class

前言

在前面一篇中主要介绍了JavaScript中使用构造函数+原型链实现继承,从实现的步骤来说还是比较繁琐的。在ES6中推出的class的关键字可以直接用来定义类,写法类似与其它的面向对象语言,但是使用class来定义的类其本质上依然是构造函数+原型链的语法糖而已,下面就一起来全面的了解一下class吧。

1.类的定义

class Person {} // 类声明
const Person = class {} // 类表达式

2.类的构造函数

  • 每个类都可以有一个自己的constructor方法,注意只能有一个,如果有多个会抛出异常;

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

    JavaScript面向对象—深入ES6的class-LMLPHP

  • 当通过new操作符来操作类时,就会去调用这个类的constructor方法,并返回一个对象(具体new操作符调用函数时的默认操作步骤在上一篇中有说明);

    class Person {
      constructor(name, age) {
        this.name = name
        this.age = age
      }
    }
    
    const p = new Person('curry', 30)
    console.log(p) // Person { name: 'curry', age: 30 }
    

3.类的实例方法

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

  eating() {
    console.log(this.name + 'is eating.')
  }

  running() {
    console.log(this.name + 'is running.')
  }
}

4.类的访问器方法

class Person {
  constructor(name, age) {
    this.name = name
    this._age = 30 // 使用_定义的属性表示为私有属性,不可直接访问
  }

  get age() {
    console.log('age被访问')
    return this._age
  }

  set age(newValue) {
    console.log('age被设置')
    this._age = newValue
  }
}

const p = new Person('curry', 30)
console.log(p) // Person { name: 'curry', _age: 30 }
p.age // age被访问
p.age = 24 // age被设置
console.log(p) // Person { name: 'curry', _age: 24 }

5.类的静态方法

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

  static foo() {
    console.log('我是Person类的方法')
  }
}

Person.foo() // 我是Person类的方法

6.类的继承

6.1.extends关键字

实现Student子类继承自Person父类:

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

  eating() {
    console.log(this.name + ' is eating.')
  }
}

class Student extends Person {
  constructor(sno) {
    this.sno = sno
  }

  studying() {
    console.log(this.name + ' is studying.')
  }
}

那么子类如何使用父类的属性和方法呢?

6.2.super关键字

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

  eating() {
    console.log(this.name + ' is eating.')
  }
}

class Student extends Person {
  constructor(name, age, sno) {
    super(name, age)
    this.sno = sno
  }

  studying() {
    console.log(this.name + ' is studying.')
  }
}

const stu = new Student('curry', 30, 101111)
console.log(stu) // Student { name: 'curry', age: 30, sno: 101111 }
// 父类的方法可直接调用
stu.eating() // curry is eating.
stu.studying() // curry is studying.

但是super关键字的用途并不仅仅只有这个,super关键字一般可以在三个地方使用:

  • 子类的构造函数中(上面的用法);

  • 实例方法中:子类不仅可以重写父类中的实例方法,还可以通过super关键字复用父类实例方法中的逻辑代码;

    class Person {
      constructor(name, age) {
        this.name = name
        this.age = age
      }
    
      eating() {
        console.log(this.name + ' is eating.')
      }
    
      parentMethod() {
        console.log('父类逻辑代码1')
        console.log('父类逻辑代码2')
        console.log('父类逻辑代码3')
      }
    }
    
    class Student extends Person {
      constructor(name, age, sno) {
        super(name, age)
        this.sno = sno
      }
    
      // 直接重写父类eating方法
      eating() {
        console.log('Student is eating.')
      }
    
      // 重写父类的parentMethod方法,并且复用逻辑代码
      parentMethod() {
        // 通过super调用父类方法,实现复用
        super.parentMethod()
    
        console.log('子类逻辑代码4')
        console.log('子类逻辑代码5')
        console.log('子类逻辑代码6')
      }
    }
    

    JavaScript面向对象—深入ES6的class-LMLPHP

  • 静态方法中:用法就和实例方法的方式一样了;

    class Person {
      constructor(name, age) {
        this.name = name
        this.age = age
      }
    
      static parentMethod() {
        console.log('父类逻辑代码1')
        console.log('父类逻辑代码2')
        console.log('父类逻辑代码3')
      }
    }
    
    class Student extends Person {
      constructor(name, age, sno) {
        super(name, age)
        this.sno = sno
      }
    
      // 重写父类的parentMethod静态方法,并且复用逻辑代码
      static parentMethod() {
        // 通过super调用父类静态方法,实现复用
        super.parentMethod()
    
        console.log('子类逻辑代码4')
        console.log('子类逻辑代码5')
        console.log('子类逻辑代码6')
      }
    }
    
    Student.parentMethod()
    

    JavaScript面向对象—深入ES6的class-LMLPHP

6.3.继承内置类

比如,在Array类上扩展两个方法,一个方法获取指定数组的第一个元素,一个方法数组的最后一个元素:

class myArray extends Array {
  firstItem() {
    return this[0]
  }

  lastItem() {
    return this[this.length - 1]
  }
}

const arr = new myArray(1, 2, 3)
console.log(arr) // myArray(3) [ 1, 2, 3 ]
console.log(arr.firstItem()) // 1
console.log(arr.lastItem()) // 3

7.类的混入

看看JavaScript中通过代码如何实现混入效果:

// 封装混入Animal类的函数
function mixinClass(BaseClass) {
  // 返回一个匿名类
  return class extends BaseClass {
    running() {
      console.log('running...')
    }
  }
}

class Person {
  eating() {
    console.log('eating...')
  }
}

class Student extends Person {
  studying() {
    console.log('studying...')
  }
}

const NewStudent = mixinClass(Student)
const stu = new NewStudent
stu.running() // running...
stu.eating() // eating...
stu.studying() // studying...

混入的实现一般不常用,因为参数不太好传递,过于局限,在JavaScript中单继承已经足够用了。

8.class定义类转ES5

  • 刚开始通过执行自调用函数得到一个Person构造函数;
  • 定义的实例方法和类方法会分别收集到一个数组中,便于后面直接调用函数进行遍历添加;
  • 判断方法类型:如果是实例方法就添加到Person原型上,是类方法直接添加到Person上;
  • 所以class定义类的本质还是通过构造函数+原型链,class就是一种语法糖;

JavaScript面向对象—深入ES6的class-LMLPHP

这里可以提出一个小问题:定义在constructor外的属性最终会被添加到哪里呢?还是会被添加到类的实例化对象上,因为ES6对这样定义的属性进行了单独的处理。

class Person {
  message = 'hello world'

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

  eating() {
    console.log(this.name + ' is eating.')
  }

  static personMethod() {
    console.log('personMethod')
  }
}

const p = new Person('curry', 30)
console.log(p) // Person { message: 'hello world', name: 'curry', age: 30 }

JavaScript面向对象—深入ES6的class-LMLPHP

扩展:在上图中通过通过babel转换后的代码中,定义的Person函数前有一个/*#__PURE__*/,那么这个有什么作用呢?

  • 实际上这个符号将函数标记为了纯函数,在JavaScript中纯函数的特点就是没有副作用,不依赖于其它东西,独立性很强;
  • 在使用webpack构建的项目中,通过babel转换后的语法更有利于webpack进行tree-shaking,没有使用到的纯函数会直接在打包的时候被压缩掉,达到减小包体积效果;
03-16 02:00