2021 年 12 月 16 日

类的基本语法

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

维基百科

在实践中,我们经常需要创建许多同类对象,如用户、商品或其他任何东西。

正如我们在 构造函数,运算符“new” 一章中所了解的,new function 可以帮助我们实现这一点。

但在现代 JavaScript 中,有一个更高级的“类”构造,它引入了许多对面向对象编程有用的新特性。

“类”语法

基本语法是

class MyClass {
  // class methods
  constructor() { ... }
  method1() { ... }
  method2() { ... }
  method3() { ... }
  ...
}

然后使用 new MyClass() 创建一个包含所有已列出方法的新对象。

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

例如

class User {

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

  sayHi() {
    alert(this.name);
  }

}

// Usage:
let user = new User("John");
user.sayHi();

当调用 new User("John")

  1. 将创建一个新对象。
  2. constructor 随给定参数运行,并将其分配给 this.name

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

类方法之间无逗号

新手开发人员常犯的一个错误是在类方法之间放置逗号,这会导致语法错误。

此处的符号不要与对象字面量混淆。在类中,不需要逗号。

什么是类?

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

让我们揭开任何魔法,看看类到底是什么。这将有助于理解许多复杂方面。

在 JavaScript 中,类是一种函数。

在此,请看

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

// proof: User is a function
alert(typeof User); // function

class User {...} 构造真正做的是

  1. 创建一个名为 User 的函数,该函数成为类声明的结果。函数代码取自 constructor 方法(如果我们不编写此类方法,则假设为空)。
  2. 将类方法(例如 sayHi)存储在 User.prototype 中。

创建 new User 对象后,当我们调用其方法时,它将从原型中获取,正如 F.prototype 章节中所述。因此,对象可以访问类方法。

我们可以将 class User 声明的结果说明为

以下是用于内省它的代码

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

// class is a function
alert(typeof User); // function

// ...or, more precisely, the constructor method
alert(User === User.prototype.constructor); // true

// The methods are in User.prototype, e.g:
alert(User.prototype.sayHi); // the code of the sayHi method

// there are exactly two methods in the prototype
alert(Object.getOwnPropertyNames(User.prototype)); // constructor, sayHi

不仅仅是语法糖

有时人们说 class 是“语法糖”(旨在使内容更易于阅读的语法,但不会引入任何新内容),因为我们实际上可以在不使用 class 关键字的情况下声明相同的内容

// rewriting class User in pure functions

// 1. Create constructor function
function User(name) {
  this.name = name;
}
// a function prototype has "constructor" property by default,
// so we don't need to create it

// 2. Add the method to prototype
User.prototype.sayHi = function() {
  alert(this.name);
};

// Usage:
let user = new User("John");
user.sayHi();

此定义的结果大致相同。因此,确实有理由可以将 class 视为一种语法糖,用于定义一个构造函数及其原型方法。

尽管如此,还是有重要的区别。

  1. 首先,由 class 创建的函数由特殊内部属性 [[IsClassConstructor]]: true 标记。因此,它与手动创建它并不完全相同。

    语言在许多地方检查该属性。例如,与常规函数不同,它必须用 new 调用

    class User {
      constructor() {}
    }
    
    alert(typeof User); // function
    User(); // Error: Class constructor User cannot be invoked without 'new'

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

    class User {
      constructor() {}
    }
    
    alert(User); // class User { ... }

    还有其他区别,我们很快就会看到它们。

  2. 类方法不可枚举。类定义将 "prototype" 中所有方法的 enumerable 标志设置为 false

    这是好的,因为如果我们对一个对象执行 for..in,我们通常不想要它的类方法。

  3. 类总是 use strict。类构造中的所有代码都自动处于严格模式。

此外,class 语法带来了许多其他功能,我们将在后面探讨。

类表达式

就像函数一样,类可以在另一个表达式中定义、传递、返回、分配等。

以下是类表达式的示例

let User = class {
  sayHi() {
    alert("Hello");
  }
};

类似于命名函数表达式,类表达式可以有名称。

如果类表达式有名称,则它仅在类内部可见

// "Named Class Expression"
// (no such term in the spec, but that's similar to Named Function Expression)
let User = class MyClass {
  sayHi() {
    alert(MyClass); // MyClass name is visible only inside the class
  }
};

new User().sayHi(); // works, shows MyClass definition

alert(MyClass); // error, MyClass name isn't visible outside of the class

我们甚至可以动态地“按需”创建类,如下所示

function makeClass(phrase) {
  // declare a class and return it
  return class {
    sayHi() {
      alert(phrase);
    }
  };
}

// Create a new class
let User = makeClass("Hello");

new User().sayHi(); // Hello

getter/setter

就像字面量对象一样,类可以包含 getter/setter、计算属性等。

以下是使用 get/set 实现的 user.name 的示例

class User {

  constructor(name) {
    // invokes the setter
    this.name = name;
  }

  get name() {
    return this._name;
  }

  set name(value) {
    if (value.length < 4) {
      alert("Name is too short.");
      return;
    }
    this._name = value;
  }

}

let user = new User("John");
alert(user.name); // John

user = new User(""); // Name is too short.

从技术上讲,这种类声明通过在 User.prototype 中创建 getter 和 setter 来工作。

计算名称 […]

以下是使用方括号 [...] 计算方法名称的示例

class User {

  ['say' + 'Hi']() {
    alert("Hello");
  }

}

new User().sayHi();

这些特性很容易记住,因为它们类似于字面量对象。

类字段

旧浏览器可能需要一个 polyfill

类字段是最近添加到语言中的。

以前,我们的类只有方法。

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

例如,让我们向 class User 添加 name 属性

class User {
  name = "John";

  sayHi() {
    alert(`Hello, ${this.name}!`);
  }
}

new User().sayHi(); // Hello, John!

因此,我们只需在声明中编写 " = ",就可以了。

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

class User {
  name = "John";
}

let user = new User();
alert(user.name); // John
alert(User.prototype.name); // undefined

我们还可以使用更复杂的表达式和函数调用来分配值

class User {
  name = prompt("Name, please?", "John");
}

let user = new User();
alert(user.name); // John

使用类字段制作绑定方法

正如 函数绑定 章节中演示的那样,JavaScript 中的函数有一个动态 this。它取决于调用的上下文。

因此,如果一个对象方法被传递并调用在另一个上下文中,this 将不再引用它的对象。

例如,此代码将显示 undefined

class Button {
  constructor(value) {
    this.value = value;
  }

  click() {
    alert(this.value);
  }
}

let button = new Button("hello");

setTimeout(button.click, 1000); // undefined

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

有两种方法可以解决它,如 函数绑定 章节中所述

  1. 传递一个包装函数,例如 setTimeout(() => button.click(), 1000)
  2. 将方法绑定到对象,例如在构造函数中。

类字段提供了另一种非常优雅的语法

class Button {
  constructor(value) {
    this.value = value;
  }
  click = () => {
    alert(this.value);
  }
}

let button = new Button("hello");

setTimeout(button.click, 1000); // hello

类字段 click = () => {...} 是逐对象创建的,每个 Button 对象都有一个单独的函数,其中 this 指向该对象。我们可以到处传递 button.click,而 this 的值始终是正确的。

这在浏览器环境中特别有用,用于事件侦听器。

总结

基本类语法如下所示

class MyClass {
  prop = value; // property

  constructor(...) { // constructor
    // ...
  }

  method(...) {} // method

  get something(...) {} // getter method
  set something(...) {} // setter method

  [Symbol.iterator]() {} // method with computed name (symbol here)
  // ...
}

MyClass 在技术上是一个函数(我们将其作为 constructor 提供),而方法、getter 和 setter 写入 MyClass.prototype

在接下来的章节中,我们将了解有关类的更多信息,包括继承和其他特性。

任务

重要性:5

Clock 类(参见沙盒)是用函数式编写的。使用“类”语法重写它。

P.S. 时钟在控制台中滴答作响,打开它即可看到。

为任务打开沙盒。

class Clock {
  constructor({ template }) {
    this.template = template;
  }

  render() {
    let date = new Date();

    let hours = date.getHours();
    if (hours < 10) hours = '0' + hours;

    let mins = date.getMinutes();
    if (mins < 10) mins = '0' + mins;

    let secs = date.getSeconds();
    if (secs < 10) secs = '0' + secs;

    let output = this.template
      .replace('h', hours)
      .replace('m', mins)
      .replace('s', secs);

    console.log(output);
  }

  stop() {
    clearInterval(this.timer);
  }

  start() {
    this.render();
    this.timer = setInterval(() => this.render(), 1000);
  }
}


let clock = new Clock({template: 'h:m:s'});
clock.start();

在沙盒中打开解决方案。

教程地图

评论

在评论之前阅读此内容…
  • 如果您有改进建议,请 提交 GitHub 问题或提交拉取请求,而不是发表评论。
  • 如果您无法理解文章中的某些内容,请详细说明。
  • 要插入几行代码,请使用 <code> 标签,对于多行代码,请将它们包装在 <pre> 标签中,对于 10 行以上的代码,请使用沙盒(plnkrjsbincodepen…)