零基础学习JS--基础篇--使用类

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;
}
}

最后

前15.PNG

前16.PNG

由于文档内容过多,为了避免影响到大家的阅读体验,在此只以截图展示部分内容

开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】

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)**

  • 25
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值