Skip to content

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

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

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

为了理解这一点,让我们脱离开发过程,把目光转向现实世界。

通常,我们使用的设备都非常复杂。但是,将内部接口与外部接口分隔开来可以让我们使用它们且没有任何问题。

一个显示生活中的例子

例如,一个咖啡机。从外面看很简单:一个按钮,一个显示器,几个洞……当然,结果就是 —— 很棒的咖啡!😃

image-20220314112238261

但是在内部……(一张摘自维修手册的图片)

image-20220314112329312

有非常多的细节。但我们可以在完全不了解这些内部细节的情况下使用它。

咖啡机非常可靠,不是吗?一台咖啡机我们可以使用好几年,只有在出现问题时 —— 把它送去维修。

咖啡机的可靠性和简洁性的秘诀 —— 所有细节都经过精心校并 隐藏 在内部。

如果我们从咖啡机上取下保护罩,那么使用它将变得复杂得多(要按哪里?),并且很危险(会触电)。

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

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

内部接口和外部接口

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

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

如果我们继续用咖啡机进行类比 —— 内部隐藏的内容:锅炉管,加热元件等 —— 是咖啡机的内部接口。

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

但是从外面看,一台咖啡机被保护壳罩住了,所以没有人可以接触到其内部接口。细节信息被隐藏起来并且无法访问。我们可以通过外部接口使用它的功能。

所以,我们需要使用一个对象时只需知道它的外部接口。我们可能完全不知道它的内部是如何工作的,这太好了。

这是个概括性的介绍。

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

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

在许多其他编程语言中,还存在“受保护”的字段:只能从类的内部和基于其扩展的类的内部访问(例如私有的,但可以从继承的类进行访问)。它们对于内部接口也很有用。从某种意义上讲,它们比私有的属性和方法更为广泛,因为我们通常希望继承类来访问它们。

受保护的字段不是在语言级别的 Javascript 中实现的,但实际上它们非常方便,因为它们是在 Javascript 中模拟的类定义语法。

现在,我们将使用所有这些类型的属性在 Javascript 中制作咖啡机。咖啡机有很多细节,我们不会对它们进行全面模拟以保持简洁(尽管我们可以)。

受保护的“waterAmount”

首先,让我们做一个简单的咖啡机类:

js
class CoffeeMachine {
    waterAmount = 0; // 内部的水量

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

}

// 创建咖啡机
let coffeeMachine = new CoffeeMachine(100);

// 加水
coffeeMachine.waterAmount = 200;

现在,属性 waterAmountpower 是公共的。我们可以轻松地从外部将它们 get/set 成任何值。

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

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

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

所以我们的属性将被命名为 _waterAmount

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

}

// 创建咖啡机
let coffeeMachine = new CoffeeMachine(100);

// 加水
coffeeMachine.waterAmount = -10; // _waterAmount 将变为 0,而不是 -10
console.log(coffeeMachine.waterAmount); // 0

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

只读的“power”

对于 power 属性,让我们将它设为只读。有时候一个属性必须只能被在创建时进行设置,之后不再被修改。

咖啡机就是这种情况:功率永远不会改变。

要做到这一点,我们只需要设置 getter,而不设置 setter:

js
class CoffeeMachine {
    // ...

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

    get power() {
        return this._power;
    }

}

// 创建咖啡机
let coffeeMachine = new CoffeeMachine(100);

console.log(`Power is: ${coffeeMachine.power}W`); // 功率是:100W

coffeeMachine.power = 25; // 不会生效(因为没有 setter)
console.log(coffeeMachine.power); // 仍就是 100

Getter/setter 函数

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

但大多数时候首选 get.../set... 函数,像这样:

js
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”

A recent addition

This is a recent addition to the language. Not supported in JavaScript engines, or supported partially yet, requires polyfilling.

这儿有一个马上就会被加到规范中的已完成的 Javascript 提案,它为私有属性和方法提供语言级支持。

私有属性和方法应该以 # 开头。它们只在类的内部可被访问。

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

js
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();

// 不能从类的外部访问类的私有属性和方法
coffeeMachine.#fixWaterAmount(123); // Error
// SyntaxError: Private field '#fixWaterAmount' must be declared in an enclosing class
coffeeMachine.#waterLimit = 1000; // Error
// SyntaxError: Private field '#waterLimit' must be declared in an enclosing class

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

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

例如,让我们使 waterAmount 成为 #waterAmount 的一个访问器:

js
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;
console.log(machine.waterAmount); // 100

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

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

但是如果我们继承自 CoffeeMachine,那么我们将无法直接访问 #waterAmount。我们需要依靠 waterAmount getter/setter:

js
class CoffeeMachine {
    #waterAmount = 0;
    get waterAmount() {
        return this.#waterAmount;
    }
    set waterAmount(value) {
        if (value < 0) value = 0;
        this.#waterAmount = value;
    }
}

class MegaCoffeeMachine extends CoffeeMachine {
    method() {
        console.log(this.#waterAmount); // Error: can only access from CoffeeMachine
        // SyntaxError: Private field '#waterAmount' must be declared in an enclosing class
    }
}

在许多情况下,这种限制太严重了。如果我们扩展 CoffeeMachine,则可能有正当理由访问其内部。这就是为什么大多数时候都会使用受保护字段,即使它们不受语言语法的支持。

私有字段不能通过 this[name] 访问

私有字段很特别。

正如我们所知道的,通常我们可以使用 this[name] 访问字段:

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

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

总结

js最开始并不是一个面向对象编程的语言,就是一个浏览器上执行的脚本,但是随着技术的不断发展,js越来越强大,随之也出现了面向对象编程的语法,class、extends等。

  1. 面向对象编程的一个核心要点,封装,得考虑封装的合理性,不能封装的太严格,也不能封装的太简单。

  2. 面向对象编程的另一个核心要点,继承,得考虑继承过来的属性和方法的访问权限,如果全部暴露也不太好,如果全部不暴露,也不太好,所以还得有个度。

  3. 多态先不考虑了,还没学那么深,以后再研究js有没有多态。

  4. 在早期的js面向对象编程,如果全部使用this.xxx=xxx,或者类字段,xxx = xxx,这样的话,所有的属性和方法,无论是派生类,还是在外部,都可以访问到,我们知道这些就是公有的属性和变量。但是在实际开发过程中,其实有一些属性和方法,我们不想让它暴露到外面,这样很影响开发和使用(因为很有可能,这玩意就是类自己用的,我们不关心它)

  5. 所以,类的变量和属性,就应该有个权限,比如公有、受保护的、私有

  6. 公有:自己可以访问、派生类可以访问、外部也可以访问

  7. 受保护:自己可以访问、派生类可以访问、外部不可以访问

  8. 私有:只有自己可以访问

  9. js中正常的this.xxx = xxx和类字段xxx = xxx,默认都是公有的

  10. js中私有的属性和方法,使用#开头命名,只能在类内部使用,这是正在提上议程的规范,兼容性还不太好,需要使用polyfill

    js
    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();
    
    // 不能从类的外部访问类的私有属性和方法
    coffeeMachine.#fixWaterAmount(123); // Error
    // SyntaxError: Private field '#fixWaterAmount' must be declared in an enclosing class
    coffeeMachine.#waterLimit = 1000; // Error
    // SyntaxError: Private field '#waterLimit' must be declared in an enclosing class
  11. 受保护的字段和方法,js中没有规定,但是它很重要,因为私有的,限制的太狠,有时候,就得用受保护的,所以我们程序员一般约定俗成的以_下划线定义的属性和方法,为受保护的,即只有自己访问或者派生类访问(咱们自己规定的,你要是直接在外部访问或着修改它,那也没办法,但是别这么搞,显得很没水平,还会出错。)

    受保护的字段一般搭配getter/setter使用。

    js
    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;
        }
    
    }
    
    // 创建咖啡机
    let coffeeMachine = new CoffeeMachine(100);
    
    // 加水
    coffeeMachine.waterAmount = -10; // _waterAmount 将变为 0,而不是 -10
    console.log(coffeeMachine.waterAmount); // 0

    Getter/setter 函数

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

    但大多数时候首选 get.../set... 函数,像这样:

    js
    class CoffeeMachine {
      _waterAmount = 0;
    
      setWaterAmount(value) {
        if (value < 0) value = 0;
        this._waterAmount = value;
      }
    
      getWaterAmount() {
        return this._waterAmount;
      }
    }
    
    new CoffeeMachine().setWaterAmount(100);

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

    另一方面,get/set 语法更短,所以最终没有严格的规定,而是由你自己来决定。

参考

https://zh.javascript.info/private-protected-properties-methods