原型继承可以模拟经典类继承。为了将传统的类引入JavaScript, ES2015 标准引入了class语法,其底层实现还是基于原型,只是原型继承的语法糖。

这篇文章将告诉你:如何定义类,初始化实例,定义字段和方法,理解私有和公共字段,掌握静态字段和方法。

 

1. 定义:类关键字

class User {
  // 类的主体
}

ES6 模块的一部分,默认导出语法如下:

export default class User {
  // 主体
}
export class User {
  // 主体
}

例如,可以使用new操作符实例化User类:

const myUser = new User();

 

2. 初始化:constructor()

constructor(param1, param2, ...)是用于初始化实例的类主体中的一种特殊方法。在这里可以设置字段的初始值或进行任何类型的对象设置。

class User {
  constructor(name) {
    this.name = name;
  }
}

 

3.字段

类字段是保存信息的变量,字段可以附加到两个实体:

  1. 类实例上的字段

  2. 类本身的字段(也称为静态字段)

字段有两种级别可访问性:

  1. public:该字段可以在任何地方访问

  2. private:字段只能在类的主体中访问

3.1 公共实例字段

class fields proposal 提案允许我们在类的主体中定义字段,并且可以立即指定初始值:

class SomeClass {
  field1;
  field2 = 'Initial value';

  // ...
}

3.2 私有实例字段

封装是一个重要的概念,它允许我们隐藏类的内部细节。使用封装类只依赖类提供的公共接口,而不耦合类的实现细节。

当实现细节改变时,考虑到封装而组织的类更容易更新。

隐藏对象内部数据的一种好方法是使用私有字段。这些字段只能在它们所属的类中读取和更改。类的外部世界不能直接更改私有字段。

私有字段只能在类的主体中访问。
class User {
  #name;

  constructor (name) {
    this.#name = name;
  }

  getName() {
    return this.#name;
  }
}

const user = new User('hong')
user.getName() // => 'hong'

user.#name  // 抛出语法错误

#name是一个私有字段。可以在User内访问和修改#name。方法getName()可以访问私有字段#name

但是,如果我们试图在 User 主体之外访问私有字段#name,则会抛出一个语法错误:SyntaxError: Private field '#name' must be declared in an enclosing class

3.3 公共静态字段

class User {
  static TYPE_ADMIN = 'admin';
  static TYPE_REGULAR = 'regular';

  name;
  type;

  constructor(name, type) {
    this.name = name;
    this.type = type;
  }
}

const admin = new User('hong', User.TYPE_ADMIN);
admin.type === User.TYPE_ADMIN; // => true

static TYPE_ADMINstatic TYPE_REGULARUser类内部定义了静态变量。要访问静态字段,必须使用后跟字段名称的类:User.TYPE_ADMINUser.TYPE_REGULAR

3.4 私有静态字段

有时,我们也想隐藏静态字段的实现细节,在时候,就可以将静态字段设为私有。

要使静态字段成为私有的,只要字段名前面加上#符号:static #myPrivateStaticField

假设我们希望限制User类的实例数量。要隐藏实例限制的详细信息,可以创建私有静态字段:

class User {
  static #MAX_INSTANCES = 2;
  static #instances = 0;

  name;

  constructor(name) {
    User.#instances++;
    if (User.#instances > User.#MAX_INSTANCES) {
      throw new Error('Unable to create User instance');
    }
    this.name = name;
  }
}

new User('张三');
new User('李四');
new User('王五'); // throws Error

静态字段User.#MAX_INSTANCES设置允许的最大实例数,而User.#instances静态字段则计算实际的实例数。

这些私有静态字段只能在User类中访问,类的外部都不会干扰限制机制:这就是封装的好处。

4.方法

4.1 实例方法

例如,定义一个方法getName(),它返回User类中的name :

class User {
  name = '无名氏';

  constructor(name) {
    this.name = name;
  }

  getName() {
    return this.name;
  }
}

const user = new User('hong');
user.getName(); // => 'hong'

方法也可以是私有的。为了使方法私有前缀,名称以#开头即可,如下所示:

class User {
  #name;

  constructor(name) {
    this.#name = name;
  }

  #getName() {
    return this.#name;
  }

  nameContains(str) {
    return this.#getName().includes(str);
  }
}

const user = new User('hong');
user.nameContains('ong');   // => true
user.nameContains('ll'); // => false

user.#getName(); // SyntaxError is thrown

4.2 getters 和 setters

class User {
  #nameValue;

  constructor(name) {
    this.name = name;
  }

  get name() {
    return this.#nameValue;
  }

  set name(name) {
    if (name === '') {
      throw new Error(`name field of User cannot be empty`);
    }
    this.#nameValue = name;
  }
}

const user = new User('hong');
user.name; // getter 被调用, => 'hong'
user.name = 'j'; // setter 被调用

user.name = ''; // setter 抛出一个错误

get name() {...} 在访问user.name会被执行。而set name(name){…}在字段更新(user.name = '前端小智')时执行。如果新值是空字符串,setter将抛出错误。

 

5. 继承: extends

JavaScript 中的类使用extends关键字支持单继承。

class Child extends Parent { }表达式中,Child类从Parent继承构造函数,字段和方法。

例如,我们创建一个新的子类ContentWriter来继承父类User

class User {
  name;

  constructor(name) {
    this.name = name;
  }

  getName() {
    return this.name;
  }
}

class ContentWriter extends User {
  posts = [];
}

const writer = new ContentWriter('John Smith');

writer.name;      // => 'John Smith'
writer.getName(); // => 'John Smith'
writer.posts;     // => []

5.1 父构造函数:`constructor()`中的`super()`

如果希望在子类中调用父构造函数,则需要使用子构造函数中可用的super()特殊函数。

例如,让ContentWriter构造函数调用User的父构造函数,以及初始化posts字段

class User {
  name;

  constructor(name) {
    this.name = name;
  }

  getName() {
    return this.name;
  }
}

class ContentWriter extends User {
  posts = [];

  constructor(name, posts) {
    super(name);
    this.posts = posts;
  }
}

const writer = new ContentWriter('hong', ['test']);
writer.name; // => 'hong'
writer.posts // =>  ['test']

子类ContentWriter中的super(name)执行父类User的构造函数。

注意,在使用this关键字之前,必须在子构造函数中执行super()。调用super()确保父构造函数初始化实例。

class Child extends Parent {
  constructor(value1, value2) {
    //无法工作
    this.prop2 = value2;
    super(value1);
  }
}

 

5.2 父实例:方法中的`super`

如果希望在子方法中访问父方法,可以使用特殊的快捷方式super

class User {
  name;

  constructor(name) {
    this.name = name;
  }

  getName() {
    return this.name;
  }
}

class ContentWriter extends User {
  posts = [];

  constructor(name, posts) {
    super(name);
    this.posts = posts;
  }

  getName() {
    const name = super.getName();
    if (name === '') {
      return '无名氏';
    }
    return name;
  }
}

const writer = new ContentWriter('hong', ['test']);
writer.getName(); // => '无名氏'

子类ContentWritergetName()直接从父类User访问方法super.getName(),这个特性称为方法重写

注意,也可以在静态方法中使用super来访问父类的静态方法。

6. 类和原型

必须说 JS 中的类语法在从原型继承中抽象方面做得很好。但是,类是在原型继承的基础上构建的。每个类都是一个函数,并在作为构造函数调用时创建一个实例。

以下两个代码段是等价的。

类版本:

class User {
  constructor(name) {
    this.name = name;
  }

  getName() {
    return this.name;
  }
}

const user = new User('hong');

user.getName();       // => 'hong'
user instanceof User; // => true

使用原型的版本:

function User(name) {
  this.name = name;
}

User.prototype.getName = function() {
  return this.name;
}

const user = new User('hong');

user.getName();       // => 'hong'
user instanceof User; // => true

 

posted on 2021-02-04 12:21  京鸿一瞥  阅读(265)  评论(0)    收藏  举报