From 74e7ced6b990ec83f0a4b1c56527ce6baf2b863f Mon Sep 17 00:00:00 2001 From: zaelggk Date: Sun, 15 Oct 2023 11:23:44 +0800 Subject: [PATCH] add zh_CN using_classes page --- .../guide/using_classes/figure8.1.png | Bin 0 -> 2799 bytes .../javascript/guide/using_classes/index.md | 763 ++++++++++++++++++ 2 files changed, 763 insertions(+) create mode 100644 files/zh-cn/web/javascript/guide/using_classes/figure8.1.png create mode 100644 files/zh-cn/web/javascript/guide/using_classes/index.md diff --git a/files/zh-cn/web/javascript/guide/using_classes/figure8.1.png b/files/zh-cn/web/javascript/guide/using_classes/figure8.1.png new file mode 100644 index 0000000000000000000000000000000000000000..a90cf4882a64f4251502b3f8c5c89085e2171acb GIT binary patch literal 2799 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 **注意:** 私有字段和方法是类中的新特性,在函数构造器中并没有与之等价的语法。 + +### 构造一个类 + +在声明一个类之后,你可以使用 [`new`](/zh-CN/docs/Web/JavaScript/Reference/Operators/new) 操作符来创建它的实例。 + +```js +const myInstance = new MyClass(); +console.log(myInstance.myField); // 'foo' +myInstance.myMethod(); +``` + +典型函数构造器可以使用 `new` 来构造,也可以不使用 `new` 来调用。然而,对于类的调用则必须使用 `new`,否则会导致错误。 + +```js +const myInstance = MyClass(); // TypeError:不能在没有 'new' 的情况下调用类构造函数 MyClass +``` + +### 类声明提升 + +与函数声明不同,类声明并不会被 [提升](/zh-CN/docs/Glossary/Hoisting) (或者,在某些解释器中,可以被提升,但是有暂时性死区的限制),这意味着你不能在声明之前使用类。 + +```js +new MyClass(); // ReferenceError:无法在初始化之前访问 MyClass + +class MyClass {} +``` + +该行为类似于使用 [`let`](/zh-CN/docs/Web/JavaScript/Reference/Statements/let) 和 [`const`](/zh-CN/docs/Web/JavaScript/Reference/Statements/const) 声明的变量。 + +### 类表达式 + +类似于函数,类声明也有其表达式形式。 + +```js +const MyClass = class { + // 类体... +}; +``` + +类表达式也可以有名字。表达式的名字只在类体内可见。 + +```js +const MyClass = class MyClassLongerName { + // 类体。这里 MyClass 和 MyClassLongerName 指向同一个类 +}; +new MyClassLongerName(); // ReferenceError:MyClassLongerName 未定义 +``` + +## 构造函数 + +类最重要的工作之一就是作为对象的“工厂”。例如,当我们使用 `Date` 构造函数时,我们期望它给我们一个新的对象,这个对象代表了我们传入的日期数据,而且我们可以使用该实例所暴露的其他方法来操作它。在类中,实例的创建是通过 [构造函数](/zh-CN/docs/Web/JavaScript/Reference/Classes/constructor) 来完成的。 + +例如,我们创建一个名为 `Color` 的类,它代表了一个特定的颜色。用户通过传入一个 [RGB](/zh-CN/docs/Glossary/RGB) 三元组来创建颜色。 + +```js +class Color { + constructor(r, g, b) { + // 将 RGB 值作为 `this` 的属性 + this.values = [r, g, b]; + } +} +``` + +打开你的浏览器的开发者工具,将上面的代码粘贴到控制台中,然后创建一个实例: + +```js +const red = new Color(255, 0, 0); +console.log(red); +``` + +你应该会看到如下输出: + +```plain +Object { values: (3) […] } + values: Array(3) [ 255, 0, 0 ] +``` + +你已经成功创建了一个 `Color` 实例,该实例有一个 `values` 属性,它是一个包含了你传入的 RGB 值的数组。这与下面的代码几乎是等价的: + +```js +function createColor(r, g, b) { + return { + values: [r, g, b], + }; +} +``` + +构造函数的语法与普通函数完全相同 —— 这意味着你可以使用其他语法,例如 [剩余参数](/zh-CN/docs/Web/JavaScript/Reference/Functions/rest_parameters): + +```js +class Color { + constructor(...values) { + this.values = values; + } +} + +const red = new Color(255, 0, 0); +// 创建一个与上面等价的实例 +``` + +每一次调用 `new` 都将创建一个新的实例。 + +```js +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 运算符](/zh-CN/docs/Web/JavaScript/Reference/Operators/new#description) 的描述中阅读更多关于 `new` 的内容。 + +```js +class MyClass { + constructor() { + this.myField = "foo"; + return {}; + } +} + +console.log(new MyClass().myField); // undefined +``` + +## 实例方法 + +如果一个类只有构造函数,那么它与一个只创建普通对象的 `createX` 工厂函数并没有太大的区别。然而,类的强大之处在于它们可以作为“模板”,自动将方法分配给实例。 + +例如,对于 `Date` 实例,你可以用一系列方法来获取日期的不同部分,例如 [年份](/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Date/getFullYear)、[月份](/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Date/getMonth)、[星期几](/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Date/getDay) 等等。你也可以通过 `setX` 方法来设置这些值,例如 [`setFullYear`](/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Date/setFullYear)。 + +对于我们的 `Color` 类,我们可以添加一个方法来获取红色值: + +```js +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 +``` + +没有方法的帮助,你可能会尝试在构造函数内部定义该函数: + +```js +class Color { + constructor(r, g, b) { + this.values = [r, g, b]; + this.getRed = function () { + return this.values[0]; + }; + } +} +``` + +这也是可以的。然而,这会导致每次创建 `Color` 实例时都会创建一个新的函数,即使它们都做着同样的事情! + +```js +console.log(new Color().getRed === new Color().getRed); // false +``` + +与之相反地,如果你使用方法,它将在所有实例之间共享。一个函数可以在所有实例之间共享,且在不同实例调用时其行为也不同,因为 `this` 的值不同。你也许好奇这个方法存储在 _哪里_ —— 它被定义在所有实例的原型上,即 `Color.prototype`,详情参阅 [继承与原型链](/zh-CN/docs/Web/JavaScript/Inheritance_and_the_prototype_chain)。 + +相似的,我们也可以添加一个 `setRed` 方法来设置红色值: + +```js +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` 数组呢? + +```js +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](/zh-CN/docs/Web/CSS/color_value/hsl) 而不是 RGB: + +```js +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 的简洁性,并防止在你做了一些“无害的重构”时,用户代码不至于崩溃。在类中,这是通过 [_私有字段_](/zh-CN/docs/Web/JavaScript/Reference/Classes/Private_class_fields) 来实现的。 + +私有字段是以 `#`(井号)开头的标识符。井号是这个字段名的必要部分,这也就意味着私有字段永远不会与公共属性发生命名冲突。为了在类中的任何地方引用一个私有字段,你必须在类体中 _声明_ 它(你不能在类体外部创建私有字段)。除此之外,私有字段与普通属性几乎是等价的。 + +```js +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` 是一个特殊语法,所以解释器可以在执行代码之前做一些静态分析,找到所有访问私有字段的地方。 + +```js-nolint example-bad +console.log(red.#values); // SyntaxError: 私有字段 `#values` 必须在封闭类内声明 +``` + +> **注意:** 在 Chrome 控制台中运行的代码可以在类外访问私有字段,JavaScript 为了方便调试而仅在 DevTools 中放宽了这一限制。 + +JavaScrip 中的私有字段是 _硬私有_ 的:如果类没有实现暴露这些私有字段的方法,也就没有任何机制可以从类外访问它们。这意味着你可以对类的私有字段做任何重构,只要暴露的方法的行为保持不变即可。 + +在我们将 `values` 字段私有化之后,我们可以在 `getRed` 和 `setRed` 方法中添加一些逻辑,而不仅仅是简单信息传递。例如,我们可以在 `setRed` 中添加一个检查逻辑,以确保它是一个有效的 R 值: + +```js +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("Invalid R value"); + } + this.#values[0] = value; + } +} + +const red = new Color(255, 0, 0); +red.setRed(1000); // RangeError:无效的 R 值 +``` + +如果我们暴露 `values` 属性,我们的用户就会很容易地绕过这个检查,直接给 `values[0]` 赋值,从而创建一个无效的颜色。但是通过良好封装的 API,我们可以使我们的代码更加健壮,防止下游的逻辑错误。 + +类方法可以读取其他实例的私有字段,只要它们属于同一个类即可。 + +```js +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`](/zh-CN/docs/Web/JavaScript/Reference/Operators/in) 运算符。 + +```js +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"` 的属性,而不是一个私有字段。 + +有一些限制在使用私有字段时需要注意:在单个类中,相同的名称不能声明两次,且它们不能被删除。这两种情况都会导致早期语法错误。 + +```js-nolint example-bad +class BadIdeas { + #firstName; + #firstName; // syntax error occurs here + #lastName; + constructor() { + delete this.#lastName; // also a syntax error + } +} +``` + +方法,[getters 与 setters](#accessor-fields) 也可以是私有的。当你需要类内部做一些复杂的事情,但是不希望代码的其他部分调用时,它们就很有用。 + +例如,想象一下创建 [HTML 自定义元素](/zh-CN/docs/Web/API/Web_components/Using_custom_elements) 时,当点击、触摸等事件被激活时可能会做一些有点复杂的事情。此外,当元素被点击时发生的有点复杂的事情应该限制在这个类中,因为程序的其他部分(或者不应该)永远不会访问它。 + +```js +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 字段_ 允许我们像访问“实际属性”一样操作某些东西。 + +```js +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,它将是只读的。 + +```js +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 +``` + +在 [严格模式](/zh-CN/docs/Web/JavaScript/Reference/Strict_mode) 下,`red.red = 0` 这一行将抛出类型错误:"Cannot set property red of #\ which has only a getter"。在非严格模式下,赋值将被静默忽略。 + +## 公共字段 + +我们已经见过了私有字段,对应地,还有公共字段。公共字段使得实例可以获得属性,且它们常常独立于构造函数的参数。 + +```js +class MyClass { + luckyNumber = Math.random(); +} +console.log(new MyClass().luckyNumber); // 0.5 +console.log(new MyClass().luckyNumber); // 0.3 +``` + +公共字段几乎等价于将一个属性赋值给 `this`。例如,上面的例子也可以转换为: + +```js +class MyClass { + constructor() { + this.luckyNumber = Math.random(); + } +} +``` + +## 静态属性 + +在上面的 `Date` 例子中,我们还遇到了 [`Date.now()`](/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Date/now) 方法,它返回当前日期。这个方法不属于任何日期实例 —— 它属于类本身。然而,它被放在 `Date` 类上,而不是作为全局的 `DateNow()` 函数,因为它在处理日期实例时最有用。 + +> **注意:** 一个好的习惯是给工具方法一个前缀(这也称作 “命名空间”)。例如,除了旧的、没有前缀的 [`parseInt()`](/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/parseInt) 方法之外,JavaScript 后来还添加了带有前缀的 [`Number.parseInt()`](/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Number/parseInt) 方法,以表明它是用于处理数字的。 + +[_静态属性_](/zh-CN/docs/Web/JavaScript/Reference/Classes/static) 是一组在类本身上定义的属性,而不是在类的实例上定义的属性。这些特性包括: + +- 静态方法 +- 静态字段 +- 静态 getter 与 setter + +可见,我们之前见过的所有类的特性都有其静态版本。例如,对于我们的 `Color` 类,我们可以创建一个静态方法,它检查给定的三元组是否是有效的 RGB 值: + +```js +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` 前缀,且 +- 它们不能从实例中访问。 + +```js +console.log(new Color(0, 0, 0).isValid); // undefined +``` + +有一个特殊结构叫做 [_静态初始化块_](/zh-CN/docs/Web/JavaScript/Reference/Classes/Static_initialization_blocks),它是一个在类第一次加载时运行的代码块。 + +```js +class MyClass { + static { + MyClass.myStaticProperty = "foo"; + } +} + +console.log(MyClass.myStaticProperty); // 'foo' +``` + +静态初始化块几乎等价于在类声明之后立即执行一些代码。唯一的区别是它们可以访问静态私有属性。 + +## 扩展与继承 + +类的一个关键特性(除了私有字段)是 _继承_,这意味着一个对象可以“借用”另一个对象的大部分行为,同时覆盖或增强某些部分的逻辑。 + +例如,假定我们需要为 `Color` 类引入透明度支持。我们可能会尝试添加一个新的字段来表示它的透明度: + +```js +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 value must be between 0 and 1"); + } + this.#values[3] = value; + } +} +``` + +然而,这意味着每个实例 —— 即使是大多数不透明的实例(那些 alpha 值为 1 的实例) —— 都必须有额外的 alpha 值,这并不是很优雅。此外,如果特性继续增长,我们的 `Color` 类将变得非常臃肿且难以维护。 + +所以,在面向对象编程中,我们更愿意创建一个 _派生类_。派生类可以访问父类的所有公共属性。在 JavaScript 中,派生类是通过 [`extends`](/zh-CN/docs/Web/JavaScript/Reference/Classes/extends) 子句声明的,它指示它扩展自哪个类。 + +```js +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 value must be between 0 and 1"); + } + this.#alpha = value; + } +} +``` + +有一些事情需要注意。首先,在构造器中,我们调用了 `super(r, g, b)`。在访问 `this` 之前,必须调用 [`super()`](/zh-CN/docs/Web/JavaScript/Reference/Operators/super) ,这是 JavaScript 的要求。`super()` 调用父类的构造函数来初始化 `this` —— 这里大致相当于 `this = new Color(r, g, b)`。`super()` 之前也可以有代码,但你不能在 `super()` 之前访问 `this` —— JavaScript 会阻止你访问未初始化的 `this`。 + +在父类完成对 `this` 的修改后,派生类才可以对其进行自己的逻辑。这里我们添加了一个名为 `#alpha` 的私有字段,并提供了一对 getter/setter 来与之交互。 + +派生类会继承父类的所有方法。例如,尽管 `ColorWithAlpha` 自身并没有声明一个 `get red()` getter,你仍然可以访问 `red`,因为这个行为是由父类指定的: + +```js +const color = new ColorWithAlpha(255, 0, 0, 0.5); +console.log(color.red); // 255 +``` + +派生类也可以覆盖父类的方法。例如,所有类都隐式继承自 [`Object`](/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object) 类,它定义了一些基本方法,例如 [`toString()`](/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/toString)。然而,基本的 `toString()` 方法是出了名的无用方法,因为它在大多数情况下打印 `[object Object]`: + +```js +console.log(red.toString()); // [object Object] +``` + +所以,我们可以覆盖它,以便在打印颜色时打印它的 RGB 值: + +```js +class Color { + #values; + // … + toString() { + return this.#values.join(", "); + } +} + +console.log(new Color(255, 0, 0).toString()); // '255, 0, 0' +``` + +在派生类内,你可以使用 `super` 访问父类的方法。这允许你可以在避免代码重复的情况下增强父类的方法。 + +```js +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` 时,静态方法也会继承,因此你也可以覆盖或增强它们。 + +```js +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 私有字段的一个关键特性(“硬私有”)。私有字段的有效范围被严格限制在类体内,所以 _任何_ 外部代码都无权访问。 + +```js-nolint example-bad +class ColorWithAlpha extends Color { + log() { + console.log(this.#values); // SyntaxError:私有字段 '#values' 必须在封闭类中声明 + } +} +``` + +一个类只能至多扩展自一个父类。这可以防止多重继承中的问题,例如 [菱形问题](https://en.wikipedia.org/wiki/Multiple_inheritance#The_diamond_problem)。然而,由于 JavaScript 的动态性,仍然可以通过类组合和 [mixins](/zh-CN/docs/Web/JavaScript/Reference/Classes/extends#mix-ins) 来实现多重继承的效果。 + +派生类的实例同时也是父类的实例,可用 [`instancesof`](/zh-CN/docs/Web/JavaScript/Reference/Operators/instanceof) 运算符来验证。 + +```js +const color = new ColorWithAlpha(255, 0, 0, 0.5); +console.log(color instanceof Color); // true +console.log(color instanceof ColorWithAlpha); // true +``` + +## 为什么用类? + +本指南到目前为止一直很实用:我们专注于 _如何_ 使用类,但有一个问题尚未解答:_为什么_ 要使用类?答案是:视情况而定。 + +类引入了一种 _范式_,或者说是一种组织代码的方式。类是面向对象编程的基础,而面向对象编程是建立在诸如 [继承]() 和 [多态]()(特别是 _子类型多态_)等概念之上的。然而,许多人在哲学上反对某些面向对象编程的做法,因此不使用类。 + +例如,`Date` 对象的一个令人厌恶的特性是它是 _可变的_。 + +```js +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 继承树,有五个类和三个层级](figure8.1.png) + +然而,如果我们只能继承自一个父类,我们又很难清楚地描述这种继承关系。一种常见的情况是,我们想要同时拥有来自多个类的行为。在 Java 中,我们可以利用接口;在 JavaScript 中,我们是用 mixins 来解决的。但归根结底,仍不是很方便。 + +往好的方面想,类是一种非常强大的工具,我们可以利用它提高我们组织代码的层次。例如,如若没有 `Color` 类,我们可能需要创建一堆的工具函数: + +```js +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`](/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Map) 与 [`Set`](/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Set) 类存储了许多元素,你可以通过 `get()`、`set()`、`has()` 等方法访问它们。 +- [`Date`](/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Date) 类以 Unix 时间戳的形式存储日期,并允许你格式化、更新与读取单独的日期元素。 +- [`Error`](/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Error) 类存储了特定异常的信息,包括错误消息、堆栈跟踪、原因等。它是少数几个拥有丰富继承结构的类之一:有多个内置类(例如 [`TypeError`](/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/TypeError) 与 [`ReferenceError`](/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/ReferenceError))继承自 `Error`。在发生错误时,这种继承允许细化错误的语义:每个错误类都代表一个特定类型的错误,可以很容易地通过 [`instanceof`](/zh-CN/docs/Web/JavaScript/Reference/Operators/instanceof) 进行检查。 + +JavaScript 提供了以面向对象的方式组织代码的能力,但是否并如何使用它完全取决于程序员的判断。 + +{{PreviousNext("Web/JavaScript/Guide/Working_with_objects", "Web/JavaScript/Guide/Using_promises")}}