JavaScript 类完整指南

更新日期: 2019-12-09 阅读: 3.2k 标签: 指南

JavaScript 使用原型继承:每个对象都从其原型对象继承属性和方法。在 JavaScript 中不存在 Java 或 Swift 等语言中所使用的作为创建对象 蓝图的传统类,原型继承仅处理对象。

原型继承可以模仿经典类的继承。为了将传统类引入 JavaScript,ES2015 标准引入了 class 语法:基于原型继承上的语法糖。

本文使你熟悉 JavaScript 类:如何定义类,初始化实例,定义字段和方法,了解私有字段和公共字段,掌握静态字段和方法。


1.定义:class 关键字

用特殊关键字 class 在 JavaScript 中定义一个类:

class User {
  // The body of class
}

上面的代码定义了一个类 User。大括号 { } 界定了类的主体。请注意,此语法称为 类声明 。

你没有义务指明 class 的名称。通过使用类表达式 ,你可以将类分配给变量:

const UserClass = class {
  // The body of class
};

可以轻松地将类导出为 ES2015 模块的一部分。这是 默认导出 的语法:

export default class User {
 // The body of class
}

还有一个 命名导出 :

export class User {
  // The body of class
}

当你创建类的 实例(instance) 时,该类将变得很有用。实例是一个包含类描述的数据和行为的对象。

new 运算符可在 JavaScript 中实例化该类:instance = new Class()。

例如,你可以用 new 运算符实例化 User 类:

const myUser = new User();

new User() 创建 User 类的实例。


2.初始化:constructor()

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

在以下示例中,构造函数设置了字段 name 的初始值:

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

User 的构造函数只有一个参数 name,用于设置 this.name 字段的初始值。

在构造函数中,this 值等于新创建的实例。

用于实例化类的参数成为构造函数的参数:

class User {
  constructor(name) {
    name; // => 'Jon Snow'    this.name = name;
  }
}

const user = new User('Jon Snow');

构造函数中的 name 参数的值为 'Jon Snow'。

如果你没有为该类定义构造函数,则会创建一个默认的构造函数。默认构造函数是一个空函数,它不会修改实例。

同时,一个 JavaScript 类最多可以有一个构造函数。


3. 字段

类字段是用来保存信息的变量。字段可以附加到 2 个实体:

  1. 类实例上的字段
  2. 类本身的字段(又称为静态)

这些字段还具有 2 级可访问性:

  1. 公共(public):该字段可在任何地方访问
  2. 私有(private):只能在课程正文中访问该字段

3.1 公共实例字段

让我们再次看一下之前的代码片段:

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

表达式 this.name = name 创建一个实例字段 name,并为其分配一个初始值。

稍后,你可以使用属性访问器来访问 name 字段:

const user = new User('Jon Snow');
user.name; // => 'Jon Snow'

name 是一个公共字段,你可以在 User 类主体之外访问它。

当像在前面场景中那样在构造函数内部隐式创建字段时,可能很难掌握字段列表。你必须从构造函数的代码中解密它们。

更好的方法是显式声明类字段。无论构造函数做什么,实例始终具有相同的字段集。

类字段提案允许你在类主体内定义字段。另外,你可以立即指示初始值:

class SomeClass {
  field1;  field2 = 'Initial value';
  // ...
}

让我们修改 User 类,并声明一个公共字段 name:

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

const user = new User('Jon Snow');
user.name; // => 'Jon Snow'

类中的 name; 声明了一个公共字段 name。

以这种方式声明的公共字段有很好的表现力:通过查看字段声明就能够了解该类的数据结构。

而且,可以在声明时立即初始化类字段。

class User {
  name = 'Unknown';
  constructor() {
    // No initialization
  }
}

const user = new User();
user.name; // => 'Unknown'

类中的 name ='Unknown' 声明一个字段 name 并用值 'Unknown' 初始化它。

对公有字段的访问或更新没有任何限制。你可以读取它们的值并将其分配给构造函数、方法内部以及类外部的公有字段。

3.2 私有实例字段

封装是一个重要的概念,可让你隐藏类的内部细节。使用封装类的人仅涉及该类提供的公共接口,而不会耦合到该类的实现细节。

当实现细节被更改时,考虑封装性的类更易于更新。

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

私有字段 仅可在类的正文中访问。

在字段名前加上特殊符号 # 使其私有,例如 #myField。每次使用该字段时,都必须保留前缀 #:不管是声明、读取还是修改。

确保在实例初始化时可以一次设置字段 #name:

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

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

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

user.#name;     // SyntaxError is thrown

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

如果尝试在用户类主体之外访问私有字段 #name,则会引发语法错误:SyntaxError: Private field '#name' must be declared in an enclosing class。

3.3 公共静态字段

你还可以在类本身上定义字段:静态字段 。它有助于定义类常量或存储特定于类的信息。

要在 JavaScript 类中创建静态字段,请使用特殊关键字 static ,后跟字段名称:static myStaticField。

让我们添加一个新的字段 type 来指示用户类型:admin 或 Regular。静态字段 TYPE_ADMIN 和 TYPE_REGULAR 是常量,可以方便的区分用户类型:

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('Site Admin', User.TYPE_ADMIN);
admin.type === User.TYPE_ADMIN; // => true

静态 TYPE_ADMIN 和静态 TYPE_REGULAR 定义了 User 类中的静态变量。要访问静态字段,你必须使用类,后面跟字段名称:User.TYPE_ADMIN和User.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('Jon Snow');
new User('Arya Stark');
new User('Sansa Stark'); // throws Error

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

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


4. 方法

这些字段用了保存数据。但是修改数据的能力是由属于类的特殊函数执行的:方法

JavaScript 类支持实例方法和静态方法。

4.1 实例方法

实例方法可以访问和修改实例数据。实例方法可以调用其他实例方法以及任何静态方法。

例如,让我们定义一个方法 getName() ,该方法返回 User 类中的名称:

class User {
  name = 'Unknown';

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

  getName() {    return this.name;  }}

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

getName() { ... } 是 User 类中的一种方法。 user.getName() 是方法调用:它执行该方法并返回计算出的值(如果有的话)。

在类方法以及构造函数中,this 的值等于类实例。使用 this 来访问实例数据: this.field,也可以调用其他方法:this.method()。

让我们添加一个新方法 name Contains(string),该方法有一个参数并调用另一个方法:

class User {
  name;

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

  getName() {
    return this.name;
  }

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

const user = new User('Jon Snow');
user.nameContains('Jon');   // => true
user.nameContains('Stark'); // => false

nameContains(str) { ... } 是 User 类的一种方法,它接受一个参数 str。不仅如此,它还通过执行实例this.getName() 的另一种方法来获取用户名。

方法也可以是私有的。可以通过前缀使方法私有,其名称以#开头。

让我们将 getName() 方法设为私有:

class User {
  #name;

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

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

const user = new User('Jon Snow');
user.nameContains('Jon');   // => true
user.nameContains('Stark'); // => false

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

#getName() 是私有方法。在方法 nameContains(str) 内,你可以这样调用一个私有方法:this.#getName()。

作为私有变量,不能在 User 类主体之外调用 #getName()。

4.2 Getter 和 Setter

getter 和 setter 模仿常规字段,但是对如何访问和修改字段有更多控制。

在尝试获取字段值时执行 getter,而在尝试设置值时使用 setter。

为了确保 User 的 name 属性不能为空,让我们将私有字段 #nameValue 包装在一个 getter 和 setter 中:

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('Jon Snow');
user.name; // The getter is invoked, => 'Jon Snow'
user.name = 'Jon White'; // The setter is invoked

user.name = ''; // The setter throws an Error

当你访问字段 user.name 的值时,将执行 get name() {...} getter。

在 set name(name){...} 字段 user.name ='Jon White' 更新时执行。如果新值是一个空字符串,则 setter 将引发错误。

4.3静态方法

静态方法是直接附加到类的函数。它们具有与类相关的逻辑,而不是与类的实例相关的逻辑。

要创建静态方法,请使用特殊关键字 static,后跟常规方法语法:static myStaticMethod() { ... }。

使用静态方法时,要记住两个简单的规则:

  1. 静态方法 可以访问 静态字段
  2. 静态方法 无法访问 实例字段。

让我们创建一个静态方法来检测是否已经使用了具有特定名称的 User。

class User {
  static #takenNames = [];

  static isNameTaken(name) {    return User.#takenNames.includes(name);  }
  name = 'Unknown';

  constructor(name) {
    this.name = name;
    User.#takenNames.push(name);
  }
}

const user = new User('Jon Snow');

User.isNameTaken('Jon Snow');   // => true
User.isNameTaken('Arya Stark'); // => false

isNameTaken() 是一种静态方法,它使用静态私有字段 User.#takenNames 来检查采用的名称。

静态方法可以是私有的:static #staticFunction(){...}。它们同样遵循私有规则:只能在类主体中调用私有静态方法。


5. 继承:extends

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

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

例如,让我们创建一个新的子类 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;     // => []

ContentWriter 从 User 继承构造函数,getName() 方法和 name 字段。同样,ContentWriter 类声明一个新字段 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('John Smith', ['Why I like JS']);
writer.name; // => 'John Smith'
writer.posts // => ['Why I like JS']

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

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

class Child extends Parent {
  constructor(value1, value2) {
    // Does not work!
    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 'Unknwon';
    }
    return name;
  }
}

const writer = new ContentWriter('', ['Why I like JS']);
writer.getName(); // => 'Unknwon'

子类 ContentWriter 的 getName() 直接从父类 User 访问方法 super.getName()。

此功能被称为方法覆盖

请注意,你也可以将 super 与静态方法一起使用,来访问父级的静态方法。


6. 对象类型检查:instanceof

object instanceof Class 是确定 object 是否为 Class 的实例的运算符。

让我们来看看 instanceof 运算符的作用:

class User {
  name;

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

  getName() {
    return this.name;
  }
}

const user = new User('Jon Snow');
const obj = {};

user instanceof User; // => true
obj instanceof User; // => false

user 是 User 类的实例, user instanceof User 的计算结果为 true。

空对象 {} 不是 User 的实例,对应的 obj instanceof User 是 false。

instanceof 是多态的:操作符将一个子类检测为父类的实例。

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('John Smith', ['Why I like JS']);

writer instanceof ContentWriter; // => true
writer instanceof User;          // => true

writer 是子类 ContentWriter 的一个实例。操作符 writer instanceof ContentWriter 的评估结果为 true。

同时 ContentWriter 是 User 的子类。因此 writer instanceof User 也将评估为 true。

如果你想确定实例确切的类怎么办?可以用 constructor 属性并直接与该类进行比较:

writer.constructor === ContentWriter; // => true
writer.constructor === User;          // => false


7. 类和原型

我必须说,JavaScript 中的类语法在从原型继承中进行抽象方面做得很好。为了描述 class 语法,我甚至没有使用术语原型

但是这些类是建立在原型继承之上的。每个类都是一个函数,并在作为构造函数调用时创建一个实例。

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

类版本:

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

  getName() {
    return this.name;
  }
}

const user = new User('John');

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

使用原型的版本:

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

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

const user = new User('John');

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

如果你熟悉 Java 或 Swift 语言的经典继承机制,则可以更轻松地使用类语法。

不管怎样,即便是你在 JavaScript 中使用类语法,我也建议你对原型继承有所了解。


8. 类功能的可用性

本文中介绍的课程功能涉及 ES2015 和第 3 阶段的提案。

在2019年底,class 功能分为以下两部分:


9. 结论

JavaScript 类用构造函数初始化实例,定义字段和方法。你甚至可以使用 static 关键字在类本身上附加字段和方法。

继承是使用 extends 关键字实现的:你可以轻松地从父级创建子级。super 关键字用于从子类访问父类。

要使用封装,请将字段和方法设为私有来隐藏类的内部细节。私有字段和方法名称必须以 # 开头。

JavaScript 中的类正在变得越来越易于​​使用。

作者:Dmitri Pavlutin
翻译:疯狂的技术
原文:https://dmitripavlutin.com/

本文内容仅供个人学习、研究或参考使用,不构成任何形式的决策建议、专业指导或法律依据。未经授权,禁止任何单位或个人以商业售卖、虚假宣传、侵权传播等非学习研究目的使用本文内容。如需分享或转载,请保留原文来源信息,不得篡改、删减内容或侵犯相关权益。感谢您的理解与支持!

链接: https://fly63.com/article/detial/7218

相关推荐

Quill 实践指南

很多时候 <textarea> 并不能满足我们对文本输入的需求,当我们需要为输入的文本添加格式时,我们需要使用像 quill 这样的富文本编辑器来完成富文本的输入。本文将会详细的讲解如何使用 quill 定制一个自己的富文本编辑器。

程序员聊天指南,建议先码后看

很多接触过程序员的人,都有一种体会:程序员=聊天终结者。经常用简短有力的几个字结束掉你苦心经营的聊天氛围,比如:你现在忙不忙?忙。那我真的是打扰了

RxJS响应式编程入门指南

RxJS 是 Reactive Extensions for JavaScript 的缩写,起源于 Reactive Extensions,是一个基于可观测数据流 Stream 结合观察者模式和迭代器模式的一种异步编程的应用库。RxJS 是 Reactive Extensions 在 JavaScript 上的实现

SVG入门指南

SVG,即可缩放矢量图形(Scalable Vector Graphics),是一种 XML 应用,可以以一种简洁、可移植的形式表示图形信息。目前,人们对 SVG 越来越感兴趣。大多数现代浏览器都能显示 SVG 图形,并且大多数矢量绘图软件都能导出 SVG 图形

Node.js 指南(迁移到安全的Buffer构造函数)

由于安全性和可用性问题,不建议使用 Buffer()和 new Buffer()构造函数,请改用 new Buffer.alloc()、Buffer.allocUnsafe()或 Buffer.from()构造方法。

AssemblyScript 入门指南

WebAssembly(Wasm)是 Web 浏览器中相对较新的功能,但它地扩展了把 Web 作为服务应用平台的功能潜力。对于 Web 开发人员来说,学习使用 WebAssembly 可能会有一个艰难的过程

vue 官方风格指南解析

Vue 有个官方的风格指南,我从中摘抄了些认为比较重要、自己常忽略的点,加上一些分析列出来。也可以直接去观看风格指南

Web 堆栈选择指南:JAMStack vs MEAN vs LAMP

开发人员需要做的决策有很多。当 Web 应用程序的需求确定下来之后,就该选择效率最高的 Web 技术栈了。Web 技术栈是用于创建 Web 应用程序的技术工具集。一套 Web 技术栈由 OS(操作系统)、Web 服务器

JS对象的 rest/spread 属性指南

在ES5中,咱们合并对象通常使用Lodash的_.extend(target, [sources]) 方法,在ES6中咱们使用 Object.assign(target, [sources])来合并对象,当然现在最常用应该是使用 Rest/Spread(展开运算符与剩余操作符)。

使用JavaScript进行面向对象编程的指南

一切都从对象开始。对象,即我们相互交流的一个载体,有其属性和方法。对象是面向对象编程的核心,不仅用于JavaScript,而且还适用于Java、C语言、C++等。

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!