2021 年 6 月 18 日

私有和受保护的属性和方法

面向对象编程最重要的原则之一——将内部接口与外部接口区分开来。

在开发任何比“hello world”应用程序更复杂的东西时,这是“必须”的做法。

为了理解这一点,让我们暂时放下开发,将目光转向现实世界。

通常,我们使用的设备都相当复杂。但将内部接口与外部接口区分开来,让我们能够毫无问题地使用它们。

现实生活中的示例

例如,一台咖啡机。从外部来看很简单:一个按钮,一个显示屏,几个孔……当然,结果是——美味的咖啡! :)

但在内部……(维修手册中的图片)

有很多细节。但我们可以在不知道任何信息的情况下使用它。

咖啡机非常可靠,不是吗?我们可以使用多年,只有在出现问题时才拿去修理。

咖啡机可靠且简单的秘诀——所有细节都经过精心调整并隐藏在内部。

如果我们从咖啡机上取下保护盖,那么使用它将变得更加复杂(按哪里?)和危险(可能会触电)。

正如我们所看到的,在编程中,对象就像咖啡机。

但为了隐藏内部细节,我们不会使用保护盖,而是使用语言和约定的特殊语法。

内部和外部接口

在面向对象编程中,属性和方法分为两组

  • 内部接口——方法和属性,可从类的其他方法访问,但不能从外部访问。
  • 外部接口——方法和属性,也可从类外部访问。

如果我们继续类比咖啡机——隐藏在内部的是什么:锅炉管、加热元件等——这是其内部接口。

内部接口用于对象工作,其细节相互使用。例如,锅炉管连接到加热元件。

但从外部来看,咖啡机被保护盖封闭,因此没有人可以接触到它们。细节是隐藏且不可访问的。我们可以通过外部接口使用其功能。

因此,我们使用对象所需要做的就是了解其外部接口。我们可能完全不知道它在内部是如何工作的,这很好。

这是一个一般性介绍。

在 JavaScript 中,有两种类型的对象字段(属性和方法)

  • 公共:可从任何地方访问。它们构成外部接口。到目前为止,我们只使用公共属性和方法。
  • 私有:只能从类内部访问。这些用于内部接口。

在许多其他语言中也存在“受保护的”字段:仅可从类内部和扩展它的类中访问(如私有,但加上从继承类访问)。它们对内部接口也很有用。从某种意义上说,它们比私有字段更广泛,因为我们通常希望继承类可以访问它们。

受保护的字段并未在语言级别上在 JavaScript 中实现,但在实践中它们非常方便,因此被模拟。

现在,我们将用所有这些类型的属性在 JavaScript 中制作一个咖啡机。咖啡机有很多细节,我们不会对它们进行建模以保持简单(尽管我们可以)。

保护“waterAmount”

让我们首先制作一个简单的咖啡机类

class CoffeeMachine {
  waterAmount = 0; // the amount of water inside

  constructor(power) {
    this.power = power;
    alert( `Created a coffee-machine, power: ${power}` );
  }

}

// create the coffee machine
let coffeeMachine = new CoffeeMachine(100);

// add water
coffeeMachine.waterAmount = 200;

现在,属性waterAmountpower是公共的。我们可以轻松地从外部获取/设置它们为任何值。

让我们将waterAmount属性更改为受保护的,以便对其进行更多控制。例如,我们不希望任何人将其设置为低于零。

受保护的属性通常以下划线 _ 为前缀。

这在语言级别上没有强制执行,但程序员之间有一个众所周知的约定,即不应从外部访问此类属性和方法。

因此,我们的属性将称为_waterAmount

class CoffeeMachine {
  _waterAmount = 0;

  set waterAmount(value) {
    if (value < 0) {
      value = 0;
    }
    this._waterAmount = value;
  }

  get waterAmount() {
    return this._waterAmount;
  }

  constructor(power) {
    this._power = power;
  }

}

// create the coffee machine
let coffeeMachine = new CoffeeMachine(100);

// add water
coffeeMachine.waterAmount = -10; // _waterAmount will become 0, not -10

现在访问受到控制,因此将水量设置为低于零变得不可能。

只读“power”

对于power属性,让我们使其只读。有时会发生这种情况,即属性必须仅在创建时设置,然后永远不会修改。

对于咖啡机来说,情况正是如此:功率永远不会改变。

要做到这一点,我们只需要制作 getter,而不需要 setter

class CoffeeMachine {
  // ...

  constructor(power) {
    this._power = power;
  }

  get power() {
    return this._power;
  }

}

// create the coffee machine
let coffeeMachine = new CoffeeMachine(100);

alert(`Power is: ${coffeeMachine.power}W`); // Power is: 100W

coffeeMachine.power = 25; // Error (no setter)
Getter/setter 函数

在这里,我们使用了 getter/setter 语法。

但大多数情况下,get.../set... 函数更受欢迎,如下所示

class CoffeeMachine {
  _waterAmount = 0;

  setWaterAmount(value) {
    if (value < 0) value = 0;
    this._waterAmount = value;
  }

  getWaterAmount() {
    return this._waterAmount;
  }
}

new CoffeeMachine().setWaterAmount(100);

这看起来有点长,但函数更灵活。它们可以接受多个参数(即使我们现在不需要它们)。

另一方面,get/set 语法较短,因此最终没有严格的规则,由您决定。

受保护的字段是继承的

如果我们继承class MegaMachine extends CoffeeMachine,那么没有什么可以阻止我们从新类的函数中访问this._waterAmountthis._power

因此,受保护的字段自然是可以继承的。与我们将在下面看到的私有字段不同。

私有“#waterLimit”

最近添加
这是对语言的最近添加。在 JavaScript 引擎中不支持,或部分支持,需要 polyfilling

有一个已完成的 JavaScript 提议,几乎在标准中,它为私有属性和方法提供语言级支持。

私有属性应以 # 开头。它们只能从类的内部访问。

例如,这里有一个私有 #waterLimit 属性和检查水的私有方法 #fixWaterAmount

class CoffeeMachine {
  #waterLimit = 200;

  #fixWaterAmount(value) {
    if (value < 0) return 0;
    if (value > this.#waterLimit) return this.#waterLimit;
  }

  setWaterAmount(value) {
    this.#waterLimit = this.#fixWaterAmount(value);
  }

}

let coffeeMachine = new CoffeeMachine();

// can't access privates from outside of the class
coffeeMachine.#fixWaterAmount(123); // Error
coffeeMachine.#waterLimit = 1000; // Error

在语言级别上,# 是字段为私有的特殊标志。我们无法从外部或从继承类中访问它。

私有字段与公有字段不冲突。我们可以同时拥有私有 #waterAmount 和公有 waterAmount 字段。

例如,让我们将 waterAmount 设为 #waterAmount 的访问器

class CoffeeMachine {

  #waterAmount = 0;

  get waterAmount() {
    return this.#waterAmount;
  }

  set waterAmount(value) {
    if (value < 0) value = 0;
    this.#waterAmount = value;
  }
}

let machine = new CoffeeMachine();

machine.waterAmount = 100;
alert(machine.#waterAmount); // Error

与受保护的字段不同,私有字段是由语言本身强制执行的。这是件好事。

但如果我们从 CoffeeMachine 继承,那么我们将无法直接访问 #waterAmount。我们需要依赖 waterAmount 的 getter/setter

class MegaCoffeeMachine extends CoffeeMachine {
  method() {
    alert( this.#waterAmount ); // Error: can only access from CoffeeMachine
  }
}

在许多情况下,这种限制过于严格。如果我们扩展 CoffeeMachine,我们可能有正当理由访问其内部。这就是为什么更常使用受保护字段,即使它们不受语言语法支持。

私有字段不可用作 this[name]

私有字段很特殊。

众所周知,我们通常可以使用 this[name] 访问字段

class User {
  ...
  sayHi() {
    let fieldName = "name";
    alert(`Hello, ${this[fieldName]}`);
  }
}

对于私有字段,这是不可能的:this['#name'] 不起作用。这是为了确保隐私的语法限制。

总结

在面向对象编程 (OOP) 中,将内部接口与外部接口分隔称为 封装

它带来以下好处

保护用户,让他们不会自食其果

想象一下,有一组开发人员使用咖啡机。它是由“最佳咖啡机”公司制造的,工作正常,但保护盖被取下了。因此,内部接口暴露在外。

所有开发人员都很文明——他们按照预期使用咖啡机。但其中一位,约翰,认为自己是聪明人,并在咖啡机内部做了一些调整。因此,两天后咖啡机坏了。

这肯定不是约翰的错,而是取下保护盖并让约翰进行操作的人的错。

编程中也是如此。如果一个类的用户更改了不打算从外部更改的内容,后果将不可预测。

可支持性

编程中的情况比现实生活中的咖啡机复杂,因为我们不仅仅是购买一次。代码不断地进行开发和改进。

如果我们严格地限制内部接口,那么类的开发者可以自由地更改其内部属性和方法,甚至无需告知用户。

如果你是此类类的开发者,那么很高兴知道私有方法可以安全地重命名,它们的形参可以更改,甚至可以移除,因为没有外部代码依赖于它们。

对于用户来说,当新版本发布时,它在内部可能是一个彻底的检修,但如果外部接口相同,则仍然易于升级。

隐藏复杂性

人们喜欢使用简单的东西。至少从外部来看是这样。内部是什么则是另一回事。

程序员也不例外。

当实现细节被隐藏,并且提供了一个简单、有良好文档记录的外部接口时,总是很方便的。

为了隐藏内部接口,我们使用受保护的或私有的属性

  • 受保护的字段以 _ 开头。这是一个众所周知的约定,而不是在语言级别强制执行的。程序员只能从其类和继承自它的类中访问以 _ 开头的字段。
  • 私有字段以 # 开头。JavaScript 确保我们只能从类内部访问它们。

现在,私有字段在浏览器中不受良好支持,但可以进行 polyfill。

教程地图

评论

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