【问题标题】:Extend a class with private/protected/public fields and methods使用私有/受保护/公共字段和方法扩展类
【发布时间】:2017-08-22 05:25:38
【问题描述】:

我正在尝试找到一种在 ES6 中创建 simili-abstract 类的方法。到目前为止,我尝试的所有事情总是会遇到语言和/或其语法的限制(也是我对原型设计的有限知识)。

基本循环;我们声明一个类并扩展它。最后一个类必须访问其超类中的一些字段和方法,但不是全部。它还变形了公共方法...

类声明应该是一个完美的封装,所以只有这段代码才能访问它(类似于命名空间)。


到目前为止,我在 ES5 中的实验是错误的……我非常感谢一些建议和帮助。

(function(){

    // ==================================

    function AbstractClass(params) {
        var _myParams = params;
        var _privateField = "Only AbstractClass can see me";
        this.publicField = "Everybody can see me";

        function privateFunc() {
            // Does private stuff
        }
    }
    AbstractClass.prototype.publicFunc = function() {
        // Does public stuff
        privateFunc(); // Works?
    }

    // ==================================

    function FinalClass(params) {
        // How to pass the params to the superclass?
    }
    FinalClass.prototype.publicFunc = function() {
        // Override and calls the superclass.publicFunc()?
        // How can I touch _privateField ? publicField ?
    }
    FinalClass.prototype = Object.create(AbstractClass.prototype);

    // ==================================

    var foo = new FinalClass("hello world!");
    foo.publicFunc();
})();

您能告诉我这段代码有什么问题以及如何解决吗?
额外问题:如何在 ES6 中正确执行此操作?
三重奖励:受保护的字段和方法呢?

谢谢。

【问题讨论】:

  • "类声明应该是完美的封装" - JS 提供的唯一完美的封装是作用域(ES6 对此没有任何改变)。跨度>
  • “类声明应该在一个完美的封装中”——那是不可达的(?)。您始终可以使用monkeypatch 运行时来公开您想要的任何内容。使用该语言的强项可能是合理的,而不是与之抗争并从其他不适合的语言中引入习语。
  • JS 中的“正确方法”是不要试图模拟抽象类、最终类或方法以及私有/受保护成员。
  • @GuillaumeF。我不知道你所说的“simili-abstract”是什么意思,但是 JS can do abstract classes
  • @GuillaumeF。是的是的。不,你仍然可以扩展它们,虽然它很丑。

标签: javascript ecmascript-6 prototype ecmascript-5


【解决方案1】:

这其实是一个很好的问题,我会尽量给你一个有见地的答案...

正如我已经在 Stack Overflow 上的某个地方解释过的,JavaScript 并不是真正的基于类的语言。它基于原型。这是一种完全不同的编程范式,您应该考虑到这一点。因此,当您使用 Vanilla JS 编写内容时,最好忘记(只是一点点)您对 Java 或 C++ 的了解。

但是,JavaScript 是一种非常灵活的语言,您可以根据需要进行编程。在我看来,JavaScript 编程主要有两种风格:惯用风格和经典风格。

  • idomatic 风格大量使用了对象字面量、鸭子类型、工厂函数和组合。
  • 经典风格试图模仿基于类的语言的行为,其中包含用于类的构造函数和用于封装的 IIFE(立即调用函数表达式)。它强调继承和多态性。

你想要的是一个抽象类。抽象类是不能实例化的类,只能用作派生类的模型。如果您关心严格封装,那么您可以在 ES5 中实现它:

// ==============================
// ABSTRACT "CLASS"
// ==============================

var OS = (function (n) {
  // Here "name" is private because it is encapsulated in the IIFE
  var name = "";

  // Constructor
  function OS (n) {
    // If "OS" is called with "new", throw an error
    if (this.constructor === OS) {
      throw new Error('You cannot instantiate an abstract class!');
    }
    name = n;
  }

  // We cannot call this method directly (except with "call" or "apply") because we cannot have direct instances of "OS"
  OS.prototype.boot = function () {
    return name + ' is booting...';
  };

  // This is an abstract method. It will be in the prototype of derived objects but should be overriden to work
  OS.prototype.shutdown = function () {
    throw new Error('You cannot call an abstract method!');
  };

  // Getter for "name"
  OS.prototype.getName = function () {
    return name;
  };

  // The constructor must be returned to be public
  return OS;
})();

// ==============================
// CONCRETE "CLASS"
// ==============================

var LinuxDistro = (function (name) {
  // Constructor
  function LinuxDistro(name) {
    // Here we call the constructor of "OS" without "new", so there will not be any error
    OS.call(this, name);
  }
  // Here "Linux Distro" inherits from "OS"
  LinuxDistro.prototype = Object.create(OS.prototype);
  LinuxDistro.prototype.constructor = LinuxDistro;

  // Private function/method
  function textTransform(str, style) {
    return style === 'lowercase' ? str.toLowerCase() : str.toUpperCase();
  }

  // The parent method is used and overriden
  LinuxDistro.prototype.boot = function () {
    return OS.prototype.boot.call(this) + ' Welcome to ' + textTransform(this.getName());
  };

  // The abstract method is implemented
  LinuxDistro.prototype.shutdown = function () {
    return 'Shutting down... See you soon on ' + textTransform(this.getName());
  };
  
  // The constructor must be returned to be public
  return LinuxDistro;
})();

// ==============================
// CLIENT CODE
// ==============================

var arch = new LinuxDistro('Arch Linux');

console.log(arch.getName());
console.log(arch.boot());
console.log(arch.shutdown());

现在你想用 ES6 做同样的事情。好处是 ES6 提供了很好的语法糖来处理类。同样,如果您关心严格封装,您可以使用以下实现:

// ==============================
// ABSTRACT "CLASS"
// ==============================

const OS = (n => {
  // Here "name" is private because it is encapsulated in the IIFE
  let name = "";

  class OS {
    constructor(n) {
      // If "OS" is called with "new", throw an error
      if (new.target === OS) {
        throw new Error('You cannot instantiate an abstract class!');
      }
      name = n;
    }

    // We cannot call this method directly (except with "call" or "apply") because we cannot have direct instances of "OS"
    boot() {
      return `${name} is booting...`;
    }

    // This is an abstract method. It will be in the prototype of derived objects but should be overriden to work
    shutdown() {
      throw new Error('You cannot call an abstract method!');
    }

    // Getter for "name"
    get name() {
      return name;
    }
  }

  // The class must be returned to be public
  return OS;
})();

// ==============================
// CONCRETE "CLASS"
// ==============================

const LinuxDistro = (name => {

  // Private function/method
  function textTransform(str, style) {
    return style === 'lowercase' ? str.toLowerCase() : str.toUpperCase();
  }
  
  class LinuxDistro extends OS {
    constructor(name) {
      // Here we call the constructor of "OS" without "new", so there will not be any error
      super(name);
    }

    // The parent method is used and overriden
    boot() {
      return `${super.boot()} Welcome to ${textTransform(this.name)}`;
    }

    // The abstract method is implemented
    shutdown() {
      return `Shutting down... See you soon on ${textTransform(this.name)}`;
    }
  }
  
  // The class must be returned to be public
  return LinuxDistro;
})();

// ==============================
// CLIENT CODE
// ==============================

const arch = new LinuxDistro('Arch Linux');

console.log(arch.name); // This is not a direct access to "name". The getter is used...
console.log(arch.boot());
console.log(arch.shutdown());

当然,这些 sn-ps 并不完美,可能看起来有点吓人。但由于 JavaScript 的原型性质,我认为这是我们能做的最好的事情。

您可能会看到,类成员要么是私有的(感谢 IIFE 和闭包),要么是公共的(感谢对象的创建方式,具有自己的属性和原型链)。如果你真的想要受保护的成员,那就另当别论了……

当您为 JavaScript 代码考虑 OOP 模型时,我建议您使用 TypeScript。这比上面提供的代码更方便、可读和可维护。

最后,如果你想进一步了解如何在 JavaScript 中实现所有传统的 OOP 设计模式(尤其是 GoF 模式),我邀请你看看我在 GitHub 上的一个项目:PatternifyJS

【讨论】:

  • 仅供参考,在这一行const LinuxDistro = (name => {,如果您要传入多个参数,则需要额外的一组 (),例如:const LinuxDistro = ((name,param2,param3) => {
猜你喜欢
  • 2023-04-08
  • 1970-01-01
  • 2017-12-19
  • 2012-04-10
  • 2011-07-09
  • 2020-11-25
  • 1970-01-01
  • 2021-03-19
  • 2017-01-20
相关资源
最近更新 更多