在面向对象的编程中,class 是用于创建对象的可扩展的程序代码模版,它为对象提供了状态(成员变量)的初始值和行为(成员函数或方法)的实现。

Wikipedia

在日常开发中,我们经常需要创建许多相同类型的对象,例如用户(users)、商品(goods)或者任何其他东西。

正如我们在 构造器和操作符 “new” 一章中已经学到的,new function 可以帮助我们实现这种需求。

但在现代 JavaScript 中,还有一个更高级的“类(class)”构造方式,它引入许多非常棒的新功能,这些功能对于面向对象编程很有用。

“class” 语法

基本语法是:

  1. class MyClass {
  2. // class 方法
  3. constructor() { ... }
  4. method1() { ... }
  5. method2() { ... }
  6. method3() { ... }
  7. ...
  8. }

然后使用 new MyClass() 来创建具有上述列出的所有方法的新对象。

new 会自动调用 constructor() 方法,因此我们可以在 constructor() 中初始化对象。

例如:

  1. class User {
  2. constructor(name) {
  3. this.name = name;
  4. }
  5. sayHi() {
  6. alert(this.name);
  7. }
  8. }
  9. // 用法:
  10. let user = new User("John");
  11. user.sayHi();

new User("John") 被调用:

  1. 一个新对象被创建。
  2. constructor 使用给定的参数运行,并为其分配 this.name

……然后我们就可以调用对象方法了,例如 user.sayHi

类的方法之间没有逗号

对于新手开发人员来说,常见的陷阱是在类的方法之间放置逗号,这会导致语法错误。

不要把这里的符号与对象字面量相混淆。在类中,不需要逗号。

什么是 class?

所以,class 到底是什么?正如人们可能认为的那样,这不是一个全新的语言级实体。

让我们揭开其神秘面纱,看看类究竟是什么。这将有助于我们理解许多复杂的方面。

在 JavaScript 中,类是一种函数。

看看下面这段代码:

  1. class User {
  2. constructor(name) { this.name = name; }
  3. sayHi() { alert(this.name); }
  4. }
  5. // 佐证:User 是一个函数
  6. alert(typeof User); // function

class User {...} 构造实际上做了如下的事儿:

  1. 创建一个名为 User 的函数,该函数成为类声明的结果。该函数的代码来自于 constructor 方法(如果我们不编写这种方法,那么它就被假定为空)。
  2. 存储类中的方法,例如 User.prototype 中的 sayHi

new User 对象被创建后,当我们调用其方法时,它会从原型中获取对应的方法,正如我们在 F.prototype 一章中所讲的那样。因此,对象 new User 可以访问类中的方法。

我们可以将 class User 声明的结果解释为:

Class 基本语法 - 图1

下面这些代码很好地解释了它们:

  1. class User {
  2. constructor(name) { this.name = name; }
  3. sayHi() { alert(this.name); }
  4. }
  5. // class 是函数 function
  6. alert(typeof User); // function
  7. // ...或者,更确切地说,是构造器方法
  8. alert(User === User.prototype.constructor); // true
  9. // 方法在 User.prototype 中,例如:
  10. alert(User.prototype.sayHi); // alert(this.name);
  11. // 在原型中实际上有两个方法
  12. alert(Object.getOwnPropertyNames(User.prototype)); // constructor, sayHi

不仅仅是语法糖

人们常说 class 是一个语法糖(旨在使内容更易阅读,但不引入任何新内容的语法),因为我们实际上可以在没有 class 的情况下声明相同的内容:

  1. // 用纯函数重写 class User
  2. // 1. 创建构造器函数
  3. function User(name) {
  4. this.name = name;
  5. }
  6. // 任何函数原型默认都具有构造器属性,
  7. // 所以,我们不需要创建它
  8. // 2. 将方法添加到原型
  9. User.prototype.sayHi = function() {
  10. alert(this.name);
  11. };
  12. // 用法:
  13. let user = new User("John");
  14. user.sayHi();

这个定义的结果与使用类得到的结果基本相同。因此,这确实是将 class 视为一种定义构造器及其原型方法的语法糖的理由。

尽管,它们之间存在着重大差异:

  1. 首先,通过 class 创建的函数具有特殊的内部属性标记 [[FunctionKind]]:"classConstructor"。因此,它与手动创建并不完全相同。

    不像普通函数,调用类构造器时必须要用 new 关键词:

    1. class User {
    2. constructor() {}
    3. }
    4. alert(typeof User); // function
    5. User(); // Error: Class constructor User cannot be invoked without 'new'

    此外,大多数 JavaScript 引擎中的类构造器的字符串表示形式都以 “class…” 开头

    1. class User {
    2. constructor() {}
    3. }
    4. alert(User); // class User { ... }
  2. 类方法不可枚举。 类定义将 "prototype" 中的所有方法的 enumerable 标志设置为 false

    这很好,因为如果我们对一个对象调用 for..in 方法,我们通常不希望 class 方法出现。

  3. 类总是使用 use strict。 在类构造中的所有代码都将自动进入严格模式。

此外,class 语法还带来了许多其他功能,我们稍后将会探索它们。

类表达式

就像函数一样,类可以在另外一个表达式中被定义,被传递,被返回,被赋值等。

这是一个类表达式的例子:

  1. let User = class {
  2. sayHi() {
  3. alert("Hello");
  4. }
  5. };

类似于命名函数表达式(Named Function Expressions),类表达式可能也应该有一个名字。

如果类表达式有名字,那么该名字仅在类内部可见:

  1. // “命名类表达式(Named Class Expression)”
  2. // (规范中没有这样的术语,但是它和命名函数表达式类似)
  3. let User = class MyClass {
  4. sayHi() {
  5. alert(MyClass); // MyClass 这个名字仅在类内部可见
  6. }
  7. };
  8. new User().sayHi(); // 正常运行,显示 MyClass 中定义的内容
  9. alert(MyClass); // error,MyClass 在外部不可见

我们甚至可以动态地“按需”创建类,就像这样:

  1. function makeClass(phrase) {
  2. // 声明一个类并返回它
  3. return class {
  4. sayHi() {
  5. alert(phrase);
  6. };
  7. };
  8. }
  9. // 创建一个新的类
  10. let User = makeClass("Hello");
  11. new User().sayHi(); // Hello

Getters/setters 及其他速记

就像对象字面量,类可能包括 getters/setters,计算属性(computed properties)等。

这是一个使用 get/set 实现 user.name 的示例:

  1. class User {
  2. constructor(name) {
  3. // 调用 setter
  4. this.name = name;
  5. }
  6. get name() {
  7. return this._name;
  8. }
  9. set name(value) {
  10. if (value.length < 4) {
  11. alert("Name is too short.");
  12. return;
  13. }
  14. this._name = value;
  15. }
  16. }
  17. let user = new User("John");
  18. alert(user.name); // John
  19. user = new User(""); // Name is too short.

类声明在 User.prototype 中创建 getters 和 setters,就像这样:

  1. Object.defineProperties(User.prototype, {
  2. name: {
  3. get() {
  4. return this._name
  5. },
  6. set(name) {
  7. // ...
  8. }
  9. }
  10. });

这是一个 [...] 中有计算属性名称(computed property name)的例子:

  1. class User {
  2. ['say' + 'Hi']() {
  3. alert("Hello");
  4. }
  5. }
  6. new User().sayHi();

Class 字段

旧的浏览器可能需要 polyfill

类字段(field)是最近才添加到语言中的。

之前,类仅具有方法。

“类字段”是一种允许添加任何属性的语法。

例如,让我们在 class User 中添加一个 name 属性:

  1. class User {
  2. name = "Anonymous";
  3. sayHi() {
  4. alert(`Hello, ${this.name}!`);
  5. }
  6. }
  7. new User().sayHi();
  8. alert(User.prototype.sayHi); // 被放在 User.prototype 中
  9. alert(User.prototype.name); // undefined,没有被放在 User.prototype 中

关于类字段的重要一点是,它们设置在单个对象上的,而不是设置在 User.prototype 上的。

从技术上讲,它们是在 constructor 完成工作后被处理的。

使用类字段制作绑定方法

正如 函数绑定 一章中所讲的,JavaScript 中的函数具有动态的 this。它取决于调用上下文。

因此,如果一个对象方法被传递到某处,或者在另一个上下文中被调用,则 this 将不再是对其对象的引用。

例如,此代码将显示 undefined

  1. class Button {
  2. constructor(value) {
  3. this.value = value;
  4. }
  5. click() {
  6. alert(this.value);
  7. }
  8. }
  9. let button = new Button("hello");
  10. setTimeout(button.click, 1000); // undefined

这个问题被称为“丢失 this”。

我们在 函数绑定 一章中讲过,有两种可以修复它的方式:

  1. 传递一个包装函数,例如 setTimeout(() => button.click(), 1000)
  2. 将方法绑定到对象,例如在 constructor 中:
  1. class Button {
  2. constructor(value) {
  3. this.value = value;
  4. this.click = this.click.bind(this);
  5. }
  6. click() {
  7. alert(this.value);
  8. }
  9. }
  10. let button = new Button("hello");
  11. setTimeout(button.click, 1000); // hello

类字段为后一种解决方案提供了更优雅的语法:

  1. class Button {
  2. constructor(value) {
  3. this.value = value;
  4. }
  5. click = () => {
  6. alert(this.value);
  7. }
  8. }
  9. let button = new Button("hello");
  10. setTimeout(button.click, 1000); // hello

类字段 click = () => {...} 在每个 Button 对象上创建一个独立的函数,并将 this 绑定到该对象上。然后,我们可以将 button.click 传递到任何地方,并且它会被以正确的 this 进行调用。

这在浏览器环境中,当我们需要将一个方法设置为事件监听器时尤其有用。

总结

基本的类语法看起来像这样:

  1. class MyClass {
  2. prop = value; // 属性
  3. constructor(...) { // 构造器
  4. // ...
  5. }
  6. method(...) {} // method
  7. get something(...) {} // getter 方法
  8. set something(...) {} // setter 方法
  9. [Symbol.iterator]() {} // 有计算名称(computed name)的方法(此处为 symbol)
  10. // ...
  11. }

技术上来说,MyClass 是一个函数(我们提供作为 constructor 的那个),而 methods、getters 和 settors 都被写入了 MyClass.prototype

在下一章,我们将会进一步学习类的相关知识,包括继承和其他功能。

任务

重写为 class

重要程度: 5

Clock 类是以函数式编写的。请以 “class” 语法重写它。

P.S. 时钟在控制台(console)中滴答,打开控制台即可查看。

打开一个任务沙箱。

解决方案

  1. class Clock {
  2. constructor({ template }) {
  3. this.template = template;
  4. }
  5. render() {
  6. let date = new Date();
  7. let hours = date.getHours();
  8. if (hours < 10) hours = '0' + hours;
  9. let mins = date.getMinutes();
  10. if (mins < 10) mins = '0' + mins;
  11. let secs = date.getSeconds();
  12. if (secs < 10) secs = '0' + secs;
  13. let output = this.template
  14. .replace('h', hours)
  15. .replace('m', mins)
  16. .replace('s', secs);
  17. console.log(output);
  18. }
  19. stop() {
  20. clearInterval(this.timer);
  21. }
  22. start() {
  23. this.render();
  24. this.timer = setInterval(() => this.render(), 1000);
  25. }
  26. }
  27. let clock = new Clock({template: 'h:m:s'});
  28. clock.start();

使用沙箱打开解决方案。