JavaScript教程(十二)--- 使用‘类’

使用类

JavaScript 是一个基于原型的语言——一个对象的行为取决于它自身的属性及其原型的属性。对类来说,相较于与其他面向对象的语言,譬如 Java,创建对象的多层级结构及其属性的继承关系需要更多的代码行。本节,我们将展示如何利用类创建实例。

在许多其他语言中,(或构造函数)与对象(或实例),是两个不同的概念。在 JavaScript 中,类可以看作是已有的原型继承机制的一种抽象——所有语法都可以转换为原型继承。类本身也是不过是 JavaScript 里一种普通的值,它们有其自己的原型链。事实上,大多数 JavaScript 纯函数都可用作构造函数——你可以用 new 运算符来调用一个构造函数以创建出一个新的对象。

本教程中,我们将研究类模型的方方面面。

本章节假定你已熟悉 JavaScript 并能使用常规的对象。

类的概述

如果你已经有动手实践过 JavaScript 的经历,或是阅读指南一路过来,你可能已经用过类了,即便你还没有自己创建过。例如,你可能会对这个很熟悉:

const bigDay = new Date(2019, 6, 19);
console.log(bigDay.toLocaleDateString());
if (bigDay.getTime() < Date.now()) {
  console.log("Once upon a time...");
}

在第一行,我们创建了一个 Date 类的实例,并将其命名为 bigDay。在第二行,我们在 bigDay 实例上调用了 toLocaleDateString() 方法,并返回了一个字符串。接下来,我们对比了两个数字:一个来自于方法 getTime() 的返回值,另个一来自于对 Date 类本身的直接调用,本例为 Date.now()。 Date 是一个 JavaScript 的内建类。从这个例子中,我们可以得到一些关于类的基本概念:

  • 类通过 new 运算符创建对象。
  • 每个对象都有一些属性(数据或方法),这些属性是由类添加的。
  • 类本身也有一些属性(数据或方法),这些属性通常用于与实例进行交互。

这些对应于类的三个关键特征:

  • 构造函数;
  • 实例方法和实例字段;
  • 静态方法和静态字段。

声明一个类

类通常通过类声明来创建。

class MyClass {
  // 类体...
}

在类体内,有若干特性可用。

class MyClass {
  // 构造函数
  constructor() {
    // 构造函数体
  }
  // 实例字段
  myField = "foo";
  // 实例方法
  myMethod() {
    // myMethod 体
  }
  // 静态字段
  static myStaticField = "bar";
  // 静态方法
  static myStaticMethod() {
    // myStaticMethod 体
  }
  // 静态块
  static {
    // 静态初始化代码
  }
  // 字段、方法、静态字段、静态方法、静态块都可以使用私有形式
  #myPrivateField = "bar";
}

如果你用过早于 ES6 的版本,你可能更熟悉使用函数作为构造函数。上面的模式大致可以转换为以下函数构造器:

function MyClass() {
  this.myField = "foo";
  // 构造函数体
}
MyClass.myStaticField = "bar";
MyClass.myStaticMethod = function () {
  // myStaticMethod 体
};
MyClass.prototype.myMethod = function () {
  // myMethod 体
};

(function () {
  // 静态初始化代码
})();

备注: 私有字段和方法是类中的新特性,在函数构造器中并没有与之等价的语法。

构造一个类

在声明一个类之后,你可以使用 new 运算符来创建它的实例。

const myInstance = new MyClass();
console.log(myInstance.myField); // 'foo'
myInstance.myMethod();

典型函数构造器可以使用 new 来构造,也可以不使用 new 来调用。然而,对于类的调用则必须使用 new,否则会导致错误。

const myInstance = MyClass(); // TypeError: Class constructor MyClass cannot be invoked without 'new'

类声明提升

与函数声明不同,类声明并不会被提升(或者,在某些解释器中,可以被提升,但是有暂时性死区的限制),这意味着你不能在声明之前使用类。

new MyClass(); // ReferenceError: Cannot access 'MyClass' before initialization

class MyClass {}

该行为与使用 let 和 const 声明变量类似。

类表达式

类似于函数,类声明也有其表达式形式。

const MyClass = class {
  // 类体...
};

类表达式也可以有名字。表达式的名字只在类体内可见。

const MyClass = class MyClassLongerName {
  // 类体。这里 MyClass 和 MyClassLongerName 指向同一个类
};
new MyClassLongerName(); // ReferenceError: MyClassLongerName is not defined

构造函数

类最重要的工作之一就是作为对象的“工厂”。例如,当我们使用 Date 构造函数时,我们期望它给我们一个新的对象,这个对象代表了我们传入的日期数据,而且我们可以使用该实例所暴露的其他方法来操作它。在类中,实例的创建是通过构造函数来完成的。

例如,我们创建一个名为 Color 的类,它代表了一个特定的颜色。用户通过传入一个 RGB (en-US) 三元组来创建颜色。

class Color {
  constructor(r, g, b) {
    // 将 RGB 值作为 `this` 的属性
    this.values = [r, g, b];
  }
}

打开你的浏览器的开发者工具,将上面的代码粘贴到控制台中,然后创建一个实例:

const red = new Color(255, 0, 0);
console.log(red);

你应该会看到如下输出:

Object { values: (3) […] }
  values: Array(3) [ 255, 0, 0 ]

你已经成功创建了一个 Color 实例,该实例有一个 values 属性,它是一个包含了你传入的 RGB 值的数组。这与下面的代码几乎是等价的:

function createColor(r, g, b) {
  return {
    values: [r, g, b],
  };
}

构造函数的语法与普通函数完全相同——这意味着你可以使用其他语法,例如剩余参数:

class Color {
  constructor(...values) {
    this.values = values;
  }
}

const red = new Color(255, 0, 0);
// 创建一个与上面等价的实例

每一次调用 new 都将创建一个新的实例。

const red = new Color(255, 0, 0);
const anotherRed = new Color(255, 0, 0);
console.log(red === anotherRed); // false

在类的构造函数里,this 的值指向新创建的实例。你可以赋予它新的属性,或者读取已有的属性(尤其是方法——我们将在下一节中介绍)。

this 的值将自动作为 new 的结果返回。不建议从构造函数中返回任何值——因为如果你返回一个非原始类型的值,它将成为 new 表达式的值,而 this 的值将被丢弃。你可以在 new 运算符的描述中关于 new 的内容。

class MyClass {
  constructor() {
    this.myField = "foo";
    return {};
  }
}

console.log(new MyClass().myField); // undefined

实例方法

如果一个类只有构造函数,那么它与一个只创建普通对象的 createX 工厂函数并没有太大的区别。然而,类的强大之处在于它们可以作为“模板”,自动将方法分配给实例。

例如,对于 Date 实例,你可以用一系列方法来获取日期的不同部分,例如年份、月份、星期几等等。你也可以通过 setX 方法来设置这些值,例如 setFullYear。

对于我们的 Color 类,我们可以添加一个方法来获取红色值:

class Color {
  constructor(r, g, b) {
    this.values = [r, g, b];
  }
  getRed() {
    return this.values[0];
  }
}

const red = new Color(255, 0, 0);
console.log(red.getRed()); // 255

没有方法的帮助,你可能会尝试在构造函数内部定义该函数:

class Color {
  constructor(r, g, b) {
    this.values = [r, g, b];
    this.getRed = function () {
      return this.values[0];
    };
  }
}

这也是可以的。然而,这会导致每次创建 Color 实例时都会创建一个新的函数,即使它们都做着同样的事情!

console.log(new Color().getRed === new Color().getRed); // false

与之相反地,如果你使用方法,它将在所有实例之间共享。一个函数可以在所有实例之间共享,且在不同实例调用时其行为也不同,因为 this 的值不同。你也许好奇这个方法存储在哪里——它被定义在所有实例的原型上,即 Color.prototype,详情参阅继承与原型链。

相似的,我们也可以添加一个 setRed 方法来设置红色值:

class Color {
  constructor(r, g, b) {
    this.values = [r, g, b];
  }
  getRed() {
    return this.values[0];
  }
  setRed(value) {
    this.values[0] = value;
  }
}

const red = new Color(255, 0, 0);
red.setRed(0);
console.log(red.getRed()); // 0; 此时也即黑色

私有字段

你或许会好奇:为什么我们要费心使用 getRed 和 setRed 方法,而不是直接访问实例上的 values 数组呢?

class Color {
  constructor(r, g, b) {
    this.values = [r, g, b];
  }
}

const red = new Color(255, 0, 0);
red.values[0] = 0;
console.log(red.values[0]); // 0

在面向对象编程中,有一个叫做“封装”的哲学。这是说你不应该访问对象的底层实现,而是使用抽象方法来与之交互。例如,如果我们突然决定将颜色表示为 HSL 而不是 RGB:

class Color {
  constructor(r, g, b) {
    // values 现在是一个 HSL 数组!
    this.values = rgbToHSL([r, g, b]);
  }
  getRed() {
    return this.values[0];
  }
  setRed(value) {
    this.values[0] = value;
  }
}

const red = new Color(255, 0, 0);
console.log(red.values[0]); // 0; 不再是 255,因为 HSL 模型下纯红色的 H 分量为 0

用户对 values 数组代表 RGB 值的假设不再成立,这可能会打破他们的代码逻辑。因此,如果你是一个类的实现者,你应该隐藏实例的内部数据结构,以保持 API 的简洁性,并防止在你做了一些“无害的重构”时,用户代码不至于崩溃。在类中,这是通过私有字段来实现的。

私有字段是以 #(井号)开头的标识符。井号是这个字段名的必要部分,这也就意味着私有字段永远不会与公共属性发生命名冲突。为了在类中的任何地方引用一个私有字段,你必须在类体中声明它(你不能在类体外部创建私有字段)。除此之外,私有字段与普通属性几乎是等价的。

class Color {
  // 声明:每个 Color 实例都有一个名为 #values 的私有字段。
  #values;
  constructor(r, g, b) {
    this.#values = [r, g, b];
  }
  getRed() {
    return this.#values[0];
  }
  setRed(value) {
    this.#values[0] = value;
  }
}

const red = new Color(255, 0, 0);
console.log(red.getRed()); // 255

在类外访问私有字段会导致语法错误,且该错误可以在早期被捕获(早期语法错误)。因为 #privateField 是一个特殊语法,所以解释器可以在执行代码之前做一些静态分析,找到所有访问私有字段的地方。

console.log(red.#values); // SyntaxError: Private field '#values' must be declared in an enclosing class

备注: 在 Chrome 控制台中运行的代码可以在类外访问私有字段,JavaScript 为了方便调试而仅在 DevTools 中放宽了这一限制。

JavaScript 中的私有字段是硬私有的:如果类没有实现暴露这些私有字段的方法,也就没有任何机制可以从类外访问它们。这意味着你可以对类的私有字段做任何重构,只要暴露的方法的行为保持不变即可。

在我们将 values 字段私有化之后,我们可以在 getRed 和 setRed 方法中添加一些逻辑,而不仅仅是简单信息传递。例如,我们可以在 setRed 中添加一个检查逻辑,以确保它是一个有效的 R 值:

class Color {
  #values;
  constructor(r, g, b) {
    this.#values = [r, g, b];
  }
  getRed() {
    return this.#values[0];
  }
  setRed(value) {
    if (value < 0 || value > 255) {
      throw new RangeError("无效的 R 值");
    }
    this.#values[0] = value;
  }
}

const red = new Color(255, 0, 0);
red.setRed(1000); // RangeError:无效的 R 值

如果我们暴露 values 属性,我们的用户就会很容易地绕过这个检查,直接给 values[0] 赋值,从而创建一个无效的颜色。但是通过良好封装的 API,我们可以使我们的代码更加健壮,防止下游的逻辑错误。

类方法可以读取其他实例的私有字段,只要它们属于同一个类即可。

class Color {
  #values;
  constructor(r, g, b) {
    this.#values = [r, g, b];
  }
  redDifference(anotherColor) {
    // #values 不一定要从 this 访问:
    // 你也可以访问属于同一个类的其他实例的私有字段。
    return this.#values[0] - anotherColor.#values[0];
  }
}

const red = new Color(255, 0, 0);
const crimson = new Color(220, 20, 60);
red.redDifference(crimson); // 35

然而,若 anotherColor 并非一个 Color 实例,#values 将不存在(即使另一个类有一个同名的私有字段,它也不是同一个东西,也不能在这里访问)。访问一个不存在的私有字段会抛出错误,而不是像普通属性一样返回 undefined。如果你不知道一个对象上是否存在一个私有字段,且你希望在不使用 try/catch 来处理错误的情况下访问它,你可以使用 in 运算符。

class Color {
  #values;
  constructor(r, g, b) {
    this.#values = [r, g, b];
  }
  redDifference(anotherColor) {
    if (!(#values in anotherColor)) {
      throw new TypeError("Color instance expected");
    }
    return this.#values[0] - anotherColor.#values[0];
  }
}

备注: 请记住,# 是一种特殊的标识符语法,你不能像字符串一样使用该字段名。"#values" in anotherColor 会查找一个名为 "#values" 的属性,而不是一个私有字段。

有一些限制在使用私有字段时需要注意:在单个类中,相同的名称不能声明两次,且它们不能被删除。这两种情况都会导致早期语法错误。

class BadIdeas {
  #firstName;
  #firstName; // 这里会发生语法错误
  #lastName;
  constructor() {
    delete this.#lastName; // 也会发生语法错误
  }
}

方法、getter 与 setter 也可以是私有的。当你需要类内部做一些复杂的事情,但是不希望代码的其他部分调用时,它们就很有用。

例如,想象一下创建 HTML 自定义元素时,当点击、触摸等事件被激活时可能会做一些有点复杂的事情。此外,当元素被点击时发生的有点复杂的事情应该限制在这个类中,因为程序的其他部分(或者不应该)永远不会访问它。

class Counter extends HTMLElement {
  #xValue = 0;
  constructor() {
    super();
    this.onclick = this.#clicked.bind(this);
  }
  get #x() {
    return this.#xValue;
  }
  set #x(value) {
    this.#xValue = value;
    window.requestAnimationFrame(this.#render.bind(this));
  }
  #clicked() {
    this.#x++;
  }
  #render() {
    this.textContent = this.#x.toString();
  }
  connectedCallback() {
    this.#render();
  }
}

customElements.define("num-counter", Counter);

在这个例子中,几乎每个字段和方法都是私有的。因此,它向程序的其他部分提供了一个接口,这个接口与内置的 HTML 元素非常相似,而 Counter 的内部则不受外部影响。

getter 字段

color.getRed() 和 color.setRed() 允许我们读取和写入颜色的红色值。如果你熟悉像 Java 这样的语言,你会对这种模式非常熟悉。然而,在 JavaScript 中,使用方法来简单地访问属性仍然有些不便。getter 字段允许我们像访问“实际属性”一样操作某些东西。

class Color {
  constructor(r, g, b) {
    this.values = [r, g, b];
  }
  get red() {
    return this.values[0];
  }
  set red(value) {
    this.values[0] = value;
  }
}

const red = new Color(255, 0, 0);
red.red = 0;
console.log(red.red); // 0

这就像是对象有了一个 red 属性——但实际上,实例上并没有这样的属性!实例只有两个方法,分别以 get 和 set 为前缀,而这使得我们可以像操作属性一样操作它们。

如果一个字段仅有一个 getter 而没有 setter,它将是只读的。

class Color {
  constructor(r, g, b) {
    this.values = [r, g, b];
  }
  get red() {
    return this.values[0];
  }
}

const red = new Color(255, 0, 0);
red.red = 0;
console.log(red.red); // 255

在严格模式下,red.red = 0 这一行将抛出类型错误:“Cannot set property red of #<Color> which has only a getter”。在非严格模式下,赋值将被静默忽略。

公共字段

我们已经见过了私有字段,对应地,还有公共字段。公共字段使得实例可以获得属性,且它们常常独立于构造函数的参数。

class MyClass {
  luckyNumber = Math.random();
}
console.log(new MyClass().luckyNumber); // 0.5
console.log(new MyClass().luckyNumber); // 0.3

公共字段几乎等价于将一个属性赋值给 this。例如,上面的例子也可以转换为:

class MyClass {
  constructor() {
    this.luckyNumber = Math.random();
  }
}

静态属性

在上面的 Date 例子中,我们还遇到了 Date.now() 方法,它返回当前日期。这个方法不属于任何日期实例——它属于类本身。然而,它被放在 Date 类上,而不是作为全局的 DateNow() 函数,因为它在处理日期实例时最有用。

备注: 一个好的习惯是给工具方法一个前缀(这也称作“命名空间”)。例如,除了旧的、没有前缀的 parseInt() 方法之外,JavaScript 后来还添加了带有前缀的 Number.parseInt() 方法,以表明它是用于处理数字的。

静态属性是一组在类本身上定义的特性,而不是在类的实例上定义的特性。这些特性包括:

  • 静态方法
  • 静态字段
  • 静态 getter 与 setter

可见,我们之前见过的所有类的特性都有其静态版本。例如,对于我们的 Color 类,我们可以创建一个静态方法,它检查给定的三元组是否是有效的 RGB 值:

class Color {
  static isValid(r, g, b) {
    return r >= 0 && r <= 255 && g >= 0 && g <= 255 && b >= 0 && b <= 255;
  }
}

Color.isValid(255, 0, 0); // true
Color.isValid(1000, 0, 0); // false

静态属性与实例属性的区别在于:

  • 它们有 static 前缀,且
  • 它们不能从实例中访问。
console.log(new Color(0, 0, 0).isValid); // undefined

有一个特殊结构叫做静态初始化块 (en-US),它是一个在类第一次加载时运行的代码块。

class MyClass {
  static {
    MyClass.myStaticProperty = "foo";
  }
}

console.log(MyClass.myStaticProperty); // 'foo'

静态初始化块几乎等价于在类声明之后立即执行一些代码。唯一的区别是它们可以访问静态私有属性。

扩展与继承

类的一个关键特性(除了私有字段)是继承,这意味着一个对象可以“借用”另一个对象的大部分行为,同时覆盖或增强某些部分的逻辑。

例如,假定我们需要为 Color 类引入透明度支持。我们可能会尝试添加一个新的字段来表示它的透明度:

class Color {
  #values;
  constructor(r, g, b, a = 1) {
    this.#values = [r, g, b, a];
  }
  get alpha() {
    return this.#values[3];
  }
  set alpha(value) {
    if (value < 0 || value > 1) {
      throw new RangeError("Alpha 值必须在 0 与 1 之间");
    }
    this.#values[3] = value;
  }
}

然而,这意味着每个实例——即使是大多数不透明的实例(那些 alpha 值为 1 的实例)——都必须有额外的 alpha 值,这并不是很优雅。此外,如果特性继续增长,我们的 Color 类将变得非常臃肿且难以维护。

所以,在面向对象编程中,我们更愿意创建一个派生类。派生类可以访问父类的所有公共属性。在 JavaScript 中,派生类是通过 extends 子句声明的,它指示它扩展自哪个类。

class ColorWithAlpha extends Color {
  #alpha;
  constructor(r, g, b, a) {
    super(r, g, b);
    this.#alpha = a;
  }
  get alpha() {
    return this.#alpha;
  }
  set alpha(value) {
    if (value < 0 || value > 1) {
      throw new RangeError("Alpha 值必须在 0 与 1 之间");
    }
    this.#alpha = value;
  }
}

有一些事情需要注意。首先,在构造器中,我们调用了 super(r, g, b)。在访问 this 之前,必须调用 super(),这是 JavaScript 的要求。super() 调用父类的构造函数来初始化 this——这里大致相当于 this = new Color(r, g, b)super() 之前也可以有代码,但你不能在 super() 之前访问 this——JavaScript 会阻止你访问未初始化的 this

在父类完成对 this 的修改后,派生类才可以对其进行自己的逻辑。这里我们添加了一个名为 #alpha 的私有字段,并提供了一对 getter/setter 来与之交互。

派生类会继承父类的所有方法。例如,尽管 ColorWithAlpha 自身并没有声明一个 get red() getter,你仍然可以访问 red,因为这个行为是由父类指定的:

const color = new ColorWithAlpha(255, 0, 0, 0.5);
console.log(color.red); // 255

派生类也可以覆盖父类的方法。例如,所有类都隐式继承自 Object 类,它定义了一些基本方法,例如 toString()。然而,基本的 toString() 方法是出了名的无用方法,因为它在大多数情况下打印 [object Object]

console.log(red.toString()); // [object Object]

所以,我们可以覆盖它,以便在打印颜色时打印它的 RGB 值:

class Color {
  #values;
  // …
  toString() {
    return this.#values.join(", ");
  }
}

console.log(new Color(255, 0, 0).toString()); // '255, 0, 0'

在派生类内,你可以使用 super 访问父类的方法。这允许你可以在避免代码重复的情况下增强父类的方法。

class ColorWithAlpha extends Color {
  #alpha;
  // …
  toString() {
    // 调用父类的 toString(),并以此构建新的返回值
    return `${super.toString()}, ${this.#alpha}`;
  }
}

console.log(new ColorWithAlpha(255, 0, 0, 0.5).toString()); // '255, 0, 0, 0.5'

当你用 extends 时,静态方法也会继承,因此你也可以覆盖或增强它们。

class ColorWithAlpha extends Color {
  // ...
  static isValid(r, g, b, a) {
    // 调用父类的 isValid(),并在此基础上增强返回值
    return super.isValid(r, g, b) && a >= 0 && a <= 1;
  }
}

console.log(ColorWithAlpha.isValid(255, 0, 0, -1)); // false

派生类无权访问父类的私有字段——这是 JavaScript 私有字段的一个关键特性(“硬私有”)。私有字段的有效范围被严格限制在类体内,所以任何外部代码都无权访问。

class ColorWithAlpha extends Color {
  log() {
    console.log(this.#values); // SyntaxError: Private field '#values' must be declared in an enclosing class
  }
}

一个类只能至多扩展自一个父类。这可以防止多重继承中的问题,例如菱形问题。然而,由于 JavaScript 的动态性,仍然可以通过类组合和 mixin 来实现多重继承的效果。

派生类的实例同时也是父类的实例,可用 instancesof 运算符来验证。

const color = new ColorWithAlpha(255, 0, 0, 0.5);
console.log(color instanceof Color); // true
console.log(color instanceof ColorWithAlpha); // true

为什么用类?

本指南到目前为止一直很实用:我们专注于如何使用类,但有一个问题尚未解答:为什么要使用类?答案是:视情况而定。

类引入了一种范式,或者说是一种组织代码的方式。类是面向对象编程的基础,而面向对象编程是建立在诸如继承和多态(特别是子类型多态)等概念之上的。然而,许多人在哲学上反对某些面向对象编程的做法,因此不使用类。

例如,Date 对象的一个令人厌恶的特性是它是可变的

function incrementDay(date) {
  return date.setDate(date.getDate() + 1);
}
const date = new Date(); // 2019-06-19
const newDay = incrementDay(date);
console.log(newDay); // 2019-06-20
// 旧日期也被修改了!?
console.log(date); // 2019-06-20

可变性与内部状态是面向对象编程的重要方面,但通常会使代码难以理解——因为任何看似无害的操作都可能产生意想不到的副作用,并改变程序中其他部分的行为。

为了代码的可重复利用,我们通常会求助于扩展类,但这也会导致类的层次结构与继承关系变得复杂。

一个典型 OOP 继承树,有五个类和三个层级

然而,如果我们只能继承自一个父类,我们又很难清楚地描述这种继承关系。一种常见的情况是,我们想要同时拥有来自多个类的行为。在 Java 中,我们可以利用接口;在 JavaScript 中,我们是用 mixin 来解决的。但归根结底,仍不是很方便。

往好的方面想,类是一种非常强大的工具,我们可以利用它提高我们组织代码的层次。例如,如若没有 Color 类,我们可能需要创建一堆工具函数:

function isRed(color) {
  return color.red === 255;
}
function isValidColor(color) {
  return (
    color.red >= 0 &&
    color.red <= 255 &&
    color.green >= 0 &&
    color.green <= 255 &&
    color.blue >= 0 &&
    color.blue <= 255
  );
}
// ...

但利用类,我们可以将它们组织入 Color 命名空间内,这提高了代码的可读性。此外,私有字段允许我们隐藏内部数据结构,这使得我们可以在不破坏 API 的情况下对其进行重构。

简而言之,你应该在你想要储存一些内部数据、并暴露大量方法的时候考虑使用类。例如这些内置的 JavaScript 类:

  • Map 与 Set 类存储了许多元素,你可以通过 get()set()has() 等方法访问它们。
  • Date 类以 Unix 时间戳的形式存储日期,并允许你格式化、更新与读取单独的日期元素。
  • Error 类存储了特定异常的信息,包括错误消息、堆栈跟踪、原因等。它是少数几个拥有丰富继承结构的类之一:有多个内置类(例如 TypeError 与 ReferenceError)继承自 Error。在发生错误时,这种继承允许细化错误的语义:每个错误类都代表一个特定类型的错误,可以很容易地通过 instanceof 进行检查。

JavaScript 提供了以面向对象的方式组织代码的能力,但是否并如何使用它完全取决于程序员的判断。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/540852.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

windows搭建ftp服务器

安装iis 点击安装即可,搜索iis打开 创建ftp服务器 身份验证&#xff1a;只勾选匿名&#xff0c;不勾选基本&#xff0c;网上的很多教程会添加基本&#xff0c;导致的问题会出现浏览提需要输入账号密码的情况&#xff0c;这里为了方便大家更直观的感受FTP建立的过程&#xff0…

新手做抖店这个操作赶快停止!不听“废店”只是早晚的事!

哈喽~我是电商月月 互联网的发展&#xff0c;做什么事情都是公开透明的&#xff0c;做抖音小店也是 大家都会在网上搜索教程&#xff0c;技巧&#xff0c;但最终还是有好多人都失败了 那是因为这些流程技巧不正确吗&#xff1f;其实并不是&#xff0c;很多小技巧都是有用的&…

CTF-遗留的压缩包

题目描述&#xff1a;小蓝同学给你发来了他自己开发的网站链接&#xff0c;他说他故意留下了一个压缩包文件&#xff0c;里面有网站的源代码&#xff0c;他想考验一下你的网络安全技能。 下发容器&#xff0c;访问链接&#xff0c;发现都是无关内容 联想到标题说有遗留的压缩…

数模 线性规划模型理论与实践

线性规划模型理论与实践 1.1 线性规划问题 在人们的生产实践中&#xff0c;经常会遇到如何利用现有资源来安排生产&#xff0c;以取得最大经济效益的问题。此类问题构成了运筹学的一个重要分支一数学规划&#xff0c;而线性规划(Linear Programming 简记LP)则是数学规划的一个…

openjudge_2.5基本算法之搜索_166:The Castle

题目 166:The Castle 总时间限制: 1000ms 内存限制: 65536kB 描述 Figure 1 shows the map of a castle.Write a program that calculates how many rooms the castle hashow big the largest room is The castle is divided into m * n (m<50, n<50) square modules.…

Linux 内核学习(1) --- 时钟子系统

标题 时钟系统说明时钟树Clock Provider时钟通用数据结构clock_device 的注册clock_provider DTS配置和注册clock consumer时钟系统总结 时钟系统说明 时钟就是 SoC 中的脉搏&#xff0c;由它来控制各个部件按各自的节奏跳动。比如&#xff0c;CPU主频设置&#xff0c;串口的波…

切面条(蓝桥杯)

目录 题目 分析 代码实现 题目 一根高筋拉面&#xff0c;中间切一刀&#xff0c;可以得到2根面条。 如果先对折1次&#xff0c;中间切一刀&#xff0c;可以得到3根面条。 如果连续对折2次&#xff0c;中间切一刀&#xff0c;可以得到5根面条。 那么&#xff0c;连续对折1…

【报名指南】2023-2024学年AILD劳动技能大赛初赛报名流程

温馨提示&#xff1a; 1.AILD劳动技能大赛免费报名参赛。报名网址&#xff1a;aild.org.cn 2.报名时间即日起至5月31日。&#xff08;上海赛区线下挑战项目4月25日报名截止&#xff0c;线上挑战项目5月31日报名截止&#xff09;。 3.指导教师只能为行政备案学校的在职教师。…

C语言 数据输入输出

本文 我们来说 数据的输入与输出 及数据的运算 在程序的运算工程中 往往需要输入一些数据 而程序的运算 所得到的运算结果又需要输出给用户 因此 数据的输入与输出 就显得非常重要 在C语言中 不提供专门的输入输出语句 所有的输入输出 都是通过对标准库的调用 来实现的 一般 …

itop4412内核编译_编译自定义函数到内核

我的itop4412开发板是半路捡的&#xff0c;所以没办法加他们的售后群&#xff0c;遇到的问题只好一点点记录吧 内核驱动编译 在日常工作过程中&#xff0c;编写内核程序可能机会不多&#xff0c;但是将厂商提供的内核源码编译到固件中&#xff0c;这个技能还是必须掌握的。 i…

认识异常(1)

❤️❤️前言~&#x1f973;&#x1f389;&#x1f389;&#x1f389; hellohello~&#xff0c;大家好&#x1f495;&#x1f495;&#xff0c;这里是E绵绵呀✋✋ &#xff0c;如果觉得这篇文章还不错的话还请点赞❤️❤️收藏&#x1f49e; &#x1f49e; 关注&#x1f4a5;&a…

系统架构最佳实践 -- 一般优惠券平台系统架构设计

优惠券是商城的一种基础的营销工具&#xff0c;在目前c端用户对于电子优惠券已经非常熟悉的情况下&#xff0c;一般自营商城的营销活动系统&#xff0c;都是从优惠券开始搭建。 一、名词定义 基于个人理解&#xff0c;为方便表述&#xff0c;首先对可能产生歧义的名词进行如下…

十九.案例演示---天猫订单分析

目录 1.数据预处理 2.对订单状况进行分析 3.不同省份订单数详情 4.省份地图绘制 5.不同星期&#xff0c;订单分布 6.订单金额与订单数量 本次案例演示数据条数为:28010 import pandas as pd from pyecharts import options as optsdf_data pd.read_excel(../data/天猫订单…

【笔记】探索生成范式:大型语言模型在信息提取中的作用

探索生成范式&#xff1a;大型语言模型在信息提取中的作用 摘要介绍 &#x1f308;你好呀&#xff01;我是 是Yu欸 &#x1f30c; 2024每日百字篆刻时光&#xff0c;感谢你的陪伴与支持 ~ &#x1f680; 欢迎一起踏上探险之旅&#xff0c;挖掘无限可能&#xff0c;共同成长&am…

五、书架开发--3.弹出框功能开发、离线缓存功能开发

实现弹出框真实业务逻辑 私密阅读tab业务逻辑 1、根据点击的tab不同&#xff0c;从而展示出不同的popup弹窗 每个tab中都有自己的index&#xff0c;点击的时候获取这个index&#xff0c;就可以知道当前点击的是哪个tab&#xff0c;然后用switch-case来根据不同的index展示不…

【GD32】MQ-6丙烷检测传感器

2.34 MQ-6丙烷检测传感器 MQ-6气体传感器所使用的气敏材料是在清洁空气中电导率较低的二氧化锡(Sno2)。当传感器所处环境中存在可燃气体时&#xff0c;传感器的电导率随空气中可燃气体浓度的增加而增大。使用简单的电路即可将电导率的变化转换为该气体浓度相对应的输出信号。M…

Windows下使用PanguVip实现浮动IP

在某些高可用场景下&#xff0c;我们往往需要使用浮动IP来进行实际访问的切换&#xff0c;比如为了保证Web应用的高可用&#xff0c;当主节点宕机后&#xff0c;我们将浮动IP切换到备节点&#xff0c;那么备节点就继续可以提供服务&#xff0c;在linux下我们可以使用keepalived…

scala---基础核心知识

一、什么是scala Scala 是一种多范式的编程语言&#xff0c;其设计初衷是要集成面向对象编程和函数式编程的各种特性。Scala运行于Java平台&#xff08;Java虚拟机&#xff09;&#xff0c;并兼容现有的Java程序。 二、为什么要学习scala 1、优雅 2、速度快 3、能融合到hado…

【SpringBoot】获取参数

获取参数 传递单个参数传递多个参数传递对象后端参数重命名传递数组传递 json 数据获取 URL 中参数上传文件获取 cookie 和 session获取cookie获取session 传递单个参数 RequestMapping("/user") RestController public class UserController {// 传递单个参数Reque…

【Delphi 爬虫库 1】GET和POST方法

文章目录 1.最简单的Get方法实现2.可自定义请求头、自定义Cookie的Get方法实现3.提取响应协议头4.实现Post请求完成单词翻译 爬虫的基本原理是根据需求获取信息并返回。就像当我们感到饥饿时&#xff0c;可以选择自己烹饪食物、外出就餐&#xff0c;或者订外卖一样。在编程中&a…