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
在我们将 `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"` 的属性,而不是一个私有字段。
方法、[getter 与 setter]( ) 也可以是私有的。当你需要类内部做一些复杂的事情,但是不希望代码的其他部分调用时,它们就很有用。
#### 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` 为前缀,而这使得我们可以像操作属性一样操作它们。
解释一下:
在JavaScript类中,我们可以定义getter和setter方法来控制对象属性的读取和设置。在这个例子中,`red` 是 `Color` 类的一个实例对象,它具有一个名为 `red` 的属性。
当我们使用赋值操作符(`=`)来给对象的属性赋值时,实际上会调用属性的 setter 方法。在这里,`red.red = 0` 这行代码执行了 `red` 对象的 `red` 属性的 setter 方法,将红色分量值设置为 `0`。
因此,在这个例子中,赋值操作 `red.red = 0` 调用的是 `red` 对象的 `red` 属性的 setter 方法,而不是直接修改属性值或数组。
如果一个字段仅有一个 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
#### 公共字段
我们已经见过了私有字段,对应地,还有公共字段。公共字段使得实例可以获得属性,且它们常常独立于构造函数的参数。
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()]( )") 方法,它返回当前日期。这个方法不属于任何日期实例——它属于类本身。
[静态属性]( )是一组在类本身上定义的特性,而不是在类的实例上定义的特性。这些特性包括:
* 静态方法
* 静态字段
* 静态 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 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
下面是派生类代码:
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’
当你用 `extends` 时,静态方法也会继承,因此你也可以覆盖或增强它们。
class ColorWithAlpha extends Color {
// …
static isValid(r, g, b, a) {
// 调用父类的 isValid(),并在此基础上增强返回值
return super.isValid(r, g, b) && a >= 0 && a <= 1;
}
}
最后
由于文档内容过多,为了避免影响到大家的阅读体验,在此只以截图展示部分内容
extends` 时,静态方法也会继承,因此你也可以覆盖或增强它们。
class ColorWithAlpha extends Color {
// ...
static isValid(r, g, b, a) {
// 调用父类的 isValid(),并在此基础上增强返回值
return super.isValid(r, g, b) && a >= 0 && a <= 1;
}
}
### 最后
[外链图片转存中...(img-5PUmQiZf-1714771045169)]
[外链图片转存中...(img-LHBgjLTw-1714771045170)]
>由于文档内容过多,为了避免影响到大家的阅读体验,在此只以截图展示部分内容
>
>**[开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】](https://bbs.csdn.net/topics/618166371)**