ES6功能扩展-类

ES5中的“类”

function Person(name) {
  this.name = name
}

Person.prototype.sayName = function() {
  console.log(this.name)
}

var p = new Person('wmui')
p.sayName() // wmui
console.log(p instanceof Person) // true
console.log(p instanceof Object) // true

ES5中没有类的概念,要想实现和类相似的功能,通常是创建一个构造函数,然后把类方法赋值给构造函数的原型。许多模拟类的JS库都是基于这个模式进行开发。

类的声明

在ES6中可以用class关键字声明一个类,关键字后面紧跟着类的名字,其他部分语法类似对象字面量,但是各元素之间不需要逗号分隔

class Person {
  constructor(name) {
    this.name = name
  }
  sayName() {
    console.log(this.name)
  }
}

let p = new Person('wmui')

p.sayName()
console.log(p instanceof Person) // true
console.log(p instanceof Object) // true

通过这种方式定义类和前面的直接使用构造函数定义类的过程相似,只不过这里是使用特殊的constructor方法来定义构造函数

两者差异

类声明与自定义类型虽然很相似,但是还是有差异的

  1. 类声明不会出现函数声明提升,不能被提升到执行语句前
  2. 类声明中的所有语句自动运行在严格模式下,且无法强行脱离严格模式执行
  3. 类中所有方法都是不可枚举到的,而自定义类型中,需要通过Object.defineProperty()方法手工指定某个方法为不可枚举
  4. 每个类都有一个名为[[Construct]]的内部方法,通过关键字new调用那些不含[[Construct]]的方法会导致程序抛出错误
  5. 使用new关键字以外的其他方式调用类的构造函数,会导致程序报错
  6. 在类中修改类名会导致程序报错

了解了两种方式的差异,现在可以不用类语法编写和类等价的代码

let Person = (function(){
  'use strict';
  const Person = function(name) {
    // 确认函数是通过new关键字被调用
    if(typeof new.target === 'undefined') {
      throw new Error("Constructor must be called with new.");
    }
    this.name = name;
  }
  Object.defineProperty(Person.prototype, 'sayName', {
    value: function() {
      // 确认函数被调用时没有使用 new
      if (typeof new.target !== 'undefined') {
          throw new Error('Method cannot be called with new.');
      }
      console.log(this.name);
    },
    enumerable: false,
    writable: true,
    configurable: true
  })
  return Person;
}())

let p = new Person('wmui');
p.sayName() // wmui

尽管可以在不使用new语法的前提下实现类的所有功能,但如此一来代码变得极为复杂

常量类名

类的名称只在类中为常量,所以尽管不能在类的方法中修改类名,但可以在外部修改

class Person {
  constructor() {
    Person = 'People' // 执行时报错
  }
}

Person = 'People' // 不报错

内部的Foo就像是通过const声明的,修改它的值会导致程序抛出错误;而外部的Foo就像是通过let声明的,可以随时修改这个绑定值

类表达式

类和函数都有两种存在的形式:声明形式和表达式形式。

声明形式的函数和类都由相应的关键字(函数是function,类是class)进行定义,后面紧跟着一个标识符;表达式形式的函数和类与之类似,只是不需要在关键字后添加标识符。

类表达式的设计初衷是为了声明相应变量或传入函数作为参数

表达式语法

let Person = class {
  constructor(name) {
    this.name = name
  }
  sayName() {
    console.log(this.name)
  }
}

let p = new Person('wmui')

p.sayName()
console.log(p instanceof Person) // true
console.log(p instanceof Object) // true

这个示例等价于前面的声明形式Person类,仅在代码编写方式略有差异。

命名表达式

类与函数一样,都可以定义为命名表达式。声明时,在关键字class后添加一个标识符即可

let Person = class Person2{
  constructor(name) {
    this.name = name
  }
  sayName() {
    console.log(this.name)
  }
}

console.log(typeof Person) // function
console.log(typeof Person2) // undefined

标识符Person2只存在于类的定义中,可以在类内部的方法中使用。而在类的外部,由于不存在Person2标识符,所以typeof Person2的结果是 undefined

类表达式还可以通过立即调用构造函数创建单例。用new调用类表达式,然后通过小括号调用这个表达式

let p = new class {
  constructor(name) {
    this.name = name
  }
  sayName() {
    console.log(this.name)
  }
}('wmui')

p.sayName() // wmui

一等公民

在程序中,一等公民指的是可以传入函数,可以从函数返回,并且可以赋值给变量的值。在JS中,函数和类都是一等公民。

把类作为参数传入函数

function createObj(classDef) {
  return new classDef()
}

class Person {
  sayName() {
    console.log('wmui')
  }
}

let obj = createObj(Person)
obj.sayName() // wmui

访问器属性

类也支持访问器属性,创建getter时,需要在关键字get后紧跟一个空格和相应的标识符;创建setter时,只需把关键字get替换为set

class Person {
  constructor(name) {
    this.name = name
  }
  get myName() {
    return this.name
  }
  set myName(value) {
    this.name = value
  }
}

let descriptor = Object.getOwnPropertyDescriptor(Person.prototype, 'myName')
console.log('get' in descriptor) // true
console.log('set' in descriptor) // true

可计算成员名称

类方法和访问器属性都支持使用可计算名称。就像在对象字面量中一样,用方括号包裹一个表达式即可使用可计算名称

let methodName = 'sayName'
class Person{
  constructor(name) {
    this.name = name
  }
  [methodName]() {
    console.log(this.name)
  }
}

let p = new Person('wmui')
p.sayName() // wmui

通过相同的方式可以在访问器属性中应用可计算名称,并且可以像往常一样通过.myName访问该属性

let propertyName = 'myName'
class Person {
  constructor(name) {
    this.name = name
  }
  get [propertyName]() {
    return this.name
  }
  set [propertyName](value) {
    this.name = value
  }
}

生成器方法

在对象字面量中,可以通过在方法名前附加一个星号(*)的方式来定义生成器,在类中也可以

class MyClass {
  *createIterator() {
    yield 1;
    yield 2;
    yield 3;
  }
}
let instance = new MyClass();
let iterator = instance.createIterator();

静态成员

在ES5中,通常直接将方法添加到构造函数中来模拟静态成员

function Person(name) {
  this.name = name
}

// 静态方法
Person.testMethod = function() {
  return 'hello'
}

// 静态属性
Person.testProperty = 'hi'

// 实例方法
Person.prototype.sayName = function() {
  console.log(this.name)
}

在ES6中,可以通过添加静态注释来表示静态成员

class Person {
  constructor(name) {
    this.name = name
  }
  
  sayName() {
    console.log(this.name)
  }

  // 静态方法
  static testMethod() {
    return 'hello'
  }
  
  // 模拟静态属性
  static get testProperty() {
    return 'hi'
  }
}

console.log(Person.testMethod()) // hello
console.log(Person.testProperty) // hi

注意: ES6规定,Class内部只有静态方法,没有静态属性。所以这里是通过getter模拟的静态属性

注意: 静态成员是对象自身的属性和方法,在实例身上无法使用

继承与派生类

在ES6之前,实现类的继承要写很多代码

function Rectangle(length, width) {
  this.length = length,
  this.width = width
}
Rectangle.prototype.getArea = function() {
  return this.length * this.width
}

function Square(length) {
  Rectangle.call(this, length, length);
}
Square.prototype = Object.create(Rectangle.prototype, {
  constructor: {
    value: Square,
    enumerable: false,
    writable: true,
    configurable: true
  }
})

var square = new Square(3)
console.log(square.getArea()) // 9
console.log(square instanceof Square) // true
console.log(square instanceof Rectangle) // true

Square继承自Rectangle,核心是创建一个基于Rectangle.prototype的新对象重写Square.prototype,并且在Square中调用Rectangle.call()方法改变this指针。

使用ES6实现相同的功能,代码会精简很多

class Rectangle {
  constructor(length, width) {
    this.length = length,
    this.width = width
  }
  getArea() {
    return this.length * this.width
  }
}

class Square extends Rectangle {
  constructor(length) {
    // 想当与Rectangle.call(this, length, length)
    super(length, length)
  }
}

let square = new Square(3)
console.log(square.getArea()) // 9
console.log(square instanceof Square) // true
console.log(square instanceof Rectangle) // true

通过extends关键字指定要继承的类,子类的原型会自动调整,然后super()方法可以调用基类的构造函数

继承自其它类的类称为派生类,如果在派生类中指定了构造函数则必须调用super()方法,否则会报错。如果不使用构造函数,创建派生类的实例时会自动调用super()方法并传入所有参数。

class Rectangle {
  // 没有构造函数
}
// 等价于 
class Square extends Rectangle {
  constructor(...args) {
    super(...args)
  }
}

注意事项

  1. 只能在派生类中使用super(),在非派生类(不是extends声明的类)或函数中使用会报错

  2. 在构造函数中访问this前一定要调用super(),它负者初始化this。如果在super()前访问this会报错

  3. 如果不想在派生类的构造函数中调用super(),可以让构造函数返回一个对象

类方法遮蔽

派生类中的方法总会覆盖基类中的同名方法

class Square extends Rectangle {
  constructor(length) {
    super(length, length)
  }

  getArea() {
    return this.length * this.length;
  }
}

派生类Square中的getArea()会覆盖Rectangle中的同名方法。如果Square的实例想调用Rectangle的getArea()方法,可以通过调用super.getArea()方法间接实现

class Square extends Rectangle {
  constructor(length) {
    super(length, length)
  }

  getArea() {
    return super.getArea()
  }
}

静态成员继承

如果基类有静态成员,派生类也可以继承这些静态成员

class Rectangle {
  constructor(length, width) {
    this.length = length,
    this.width = width
  }
  getArea() {
    return this.length * this.width
  }
  static create(length, width) {
    // 该方法返回一个Rectangle实例
    return new Rectangle(length, width)
  }
}

class Square extends Rectangle {
  constructor(length) {
    super(length, length)
  }
}

let rect = Square.create(3,4)
console.log(rect.getArea()) // 12
console.log(rect instanceof Rectangle) // true
console.log(rect instanceof Square) // false

Square继承了Rectangle的create()静态方法

派生自表达式的类

派生类不一定非要是继承自class的基类,它也可以继承自某个表达式。只要表达式可以被解析为函数并且具有[[Construct]]属性和原型

function Rectangle(length, width) {
  this.length = length
  this.width = width
}

Rectangle.prototype.getArea = function() {
  return this.length * this.width
}

class Square extends Rectangle {
  constructor(length) {
    super(length, length)
  }
}

let square = new Square(3)
console.log(square.getArea()) // 9
console.log(square instanceof Rectangle) // true

Rectangle是一个ES5风格的构造函数,Square是一个类,由于Rectangle具有[[Construct]]属性和原型,因此Square类可以直接继承它

内建对象的继承

在ES5中要想实现内键对象的继承几乎不可能,比如想通过继承的方式创建基于Array的特殊数组。

function MyArray(length) {
  Array.apply(this, arguments)
}
MyArray.prototype = Object.create(Array.prototype, {
  constructor: {
    value: MyArray,
    enumerable: false,
    writable: true,
    configurable: true
  }
})

var colors = new MyArray()
colors[0] = 'red'
console.log(colors.length) // 0

colors.length的结果不是期望的1而是0,这是因为通过传统JS继承形式实现的数组继承没有从Array.apply()或原型赋值中继承数组相关功能

在ES6中可以轻松实现内建对象的继承

class MyArray extends Array {}

let colors = new MyArray()
colors[0] = 'red'
console.log(colors.length) // 1

new.target

在类的构造函数中也可以通过new.target来确定类是如何被调用的,new.target及它的值根据函数被调用的方式而改变

class Rectangle {
  constructor(length, width) {
    console.log(new.target === Rectangle);
    this.length = length;
    this.width = width;
  }
}

let rect = new Rectangle(3, 4); 
// true
class Rectangle {
  constructor(length, width) {
    console.log(new.target === Square);
    this.length = length;
    this.width = width;
  }
}
class Square extends Rectangle {
  constructor(length) {
    super(length, length)
  }
}

let square = new Square(3); // true

示例中,创建Rectangle实例时new.target等价于Rectangle,创建Square实例时new.target等价于Square

posted @ 2021-09-29 13:00  wmui  阅读(60)  评论(0编辑  收藏  举报