无法在 javascript 中拥有基于类的对象?

IT技术 javascript class oop private
2021-01-24 22:03:21

基于 javascript 原型的面向对象编程风格很有趣,但在很多情况下,您需要能够从类创建对象。

例如,在矢量绘图应用程序中,工作区在绘图开始时通常是空的:我无法从现有的“线”创建新的“线”。更一般地说,动态创建对象的每种情况都需要使用类。

我已经阅读了很多教程和“Javascript:好的部分”一书,但在我看来,没有办法定义尊重 1) 封装和 2) 高效成员方法声明的类(我的意思是:成员定义一次并在每个类实例之间共享的方法)。

为了定义私有变量,使用了闭包:

function ClassA()
{
    var value = 1;
    this.getValue = function()
    {
        return value;
    }
}

这里的问题是“ClassA”的每个实例都会有它自己的成员函数“getValue”的副本,这是效率不高的。

为了有效地定义成员函数,使用原型:

function ClassB()
{
    this.value = 1;
}

ClassB.prototype.getValue = function()
{
    return this.value;
}

这里的问题是成员变量“value”是公开的。

我认为这个问题不容易解决,因为需要在对象创建期间定义“私有”变量(以便对象可以访问其创建上下文,而不暴露这些值),而基于原型的成员函数定义必须在对象创建之后完成,这样原型才有意义(“this.prototype”不存在,我已经检查过)。

或者我错过了什么?


编辑 :

首先,感谢您的有趣回答。

我只是想为我的初始信息添加一点精确度:

我真正想做的是拥有 1) 私有变量(封装很好,因为人们只能访问他们需要的东西)和 2)有效的成员方法声明(避免复制)。

似乎简单的私有变量声明实际上只能通过 javascript 中的闭包来实现,这就是我关注基于类的方法的根本原因。如果有一种方法可以使用基于原型的方法实现简单的私有变量声明,那对我来说没问题,我不是一个激烈的基于类的方法支持者。

阅读答案后,似乎简单的解决方案是忘记私有,并使用特殊的编码约定来阻止其他程序员直接访问“私有”变量......

我同意,我的标题/第一句话对我想在这里讨论的问题具有误导性。

6个回答

嘘,过来!想听个秘密吗?

经典继承是一种经过测试和尝试的方法。

经常实现它在JavaScript中非常有用。类是一个很好的概念,并且拥有在对象之后建模我们的世界的模板很棒。

经典继承只是一种模式。如果您的用例需要这种模式,那么在 JavaScript 中实现经典继承是完全可以的。

原型继承侧重于共享功能,这很棒恐龙鼓棒很棒),但在某些情况下,您希望共享数据方案而不是功能。这是原型继承根本无法解决的问题。

所以,你告诉我课程并不像每个人一直告诉我的那样邪恶?

不,他们不是。JS 社区不赞成的不是类的概念,而是将自己限制在仅用于代码重用的类。就像语言不强制强类型或静态类型一样,它也不强制对象结构的方案。

事实上,在幕后,语言的巧妙实现可以将您的普通对象转变为类似于经典继承类的东西。

那么,类在 JavaScript 中是如何工作的

好吧,你真的只需要一个构造函数:

function getVehicle(engine){
    return { engine : engine };
}

var v = getVehicle("V6");
v.engine;//v6

我们现在有一个车辆类。我们不需要使用特殊关键字显式定义 Vehicle 类。现在,有些人不喜欢以这种方式做事,而是习惯了更经典的方式。为此 JS通过执行以下操作提供(愚蠢的 imho)语法糖

function Vehicle(engine){
     this.engine = engine;
}
var v = new Vehicle("V6");
v.engine;//v6

大多数情况下,这与上面的示例相同。

那么,我们还缺少什么?

继承和私有成员。

如果我告诉你基本的子类型在 JavaScript 中非常简单怎么办?

JavaScript 的打字概念与我们在其他语言中习惯的不同。成为 JS 中某种类型的子类型是什么意思?

var a = {x:5};
var b = {x:3,y:3};

是的类型b的子类型的类型a假设它是根据(强)行为子类型(LSP):

<<<<开始技术部分

  • 子类型中方法参数的逆变- 在这种继承中完全保留。
  • 子类型中返回类型的协方差- 在这种继承中完全保留。
  • 子类型的方法不应抛出新的异常,除非这些异常本身是超类型的方法抛出的异常的子类型。——完全保存在这种传承之中。

还,

所有这些都是再一次,由我们来保持。我们可以随心所欲地保持它们紧密或松散,我们不必这样做,但我们肯定可以。

所以事实上,只要我们在实现继承时遵守上述这些规则,我们就完全实现了强行为子类型,这是一种非常强大的子类型(见注释*)。

>>>>> 结束技术部分

简单地说,人们还可以看到结构子类型是成立的。

这将如何应用于我们的Car示例?

function getCar(typeOfCar){
    var v = getVehicle("CarEngine");
    v.typeOfCar = typeOfCar;
    return v;
}
v = getCar("Honda");
v.typeOfCar;//Honda;
v.engine;//CarEngine

不会太难吧?私人会员呢?

function getVehicle(engine){
    var secret = "Hello"
    return {
        engine : engine,
        getSecret : function() {
            return secret;
        }
    };
}

看,secret是一个闭包变量。它是完全“私有的”,它的工作方式与 Java 等语言中的私有不同,但不可能从外部访问。

在函数中使用私有怎么样?

啊! 这是一个很好的问题。

如果我们想在原型上共享的函数中使用私有变量,我们首先需要了解 JS 闭包和函数是如何工作的。

在 JavaScript 中,函数是一流的。这意味着您可以传递函数。

function getPerson(name){
    var greeting = "Hello " + name;
    return {
        greet : function() {
            return greeting;
        }
    };
}

var a = getPerson("thomasc");
a.greet(); //Hello thomasc

到目前为止一切顺利,但我们可以将绑定到 a 的函数传递给其他对象!这使您可以进行非常松散的解耦,这非常棒。

var b = a.greet;
b(); //Hello thomasc

等待!怎么b知道这个人的名字叫thomasc?这就是闭包的神奇之处。很厉害吧?

您可能会担心性能。让我告诉你我是如何学会不再担心并开始喜欢优化 JIT 的。

实际上,拥有这样的函数副本并不是什么大问题。javascript 中的函数都是关于功能的!闭包是一个很棒的概念,一旦你掌握并掌握了它们,你就会发现它是非常值得的,而对性能的影响真的没有那么重要。JS 一天比一天快,不用担心这些性能问题。

如果你觉得它很复杂,下面的也很合理。与其他开发人员的共同合同只是简单地说“如果我的变量以_不要碰它开头,我们都是同意的成年人”。这看起来像:

function getPerson(name){
    var greeter = {
        greet : function() {
            return "Hello" +greeter._name;
        }
    };
    greeter._name = name;
    return greeter;
}

或者古典风格

function Person(name){
    this._name = name;
    this.greet = function(){
       return "Hello "+this._name;
    }
}

或者,如果您想在原型上缓存函数而不是实例化副本:

function Person(name){
    this._name = name;
}
Person.prototype.greet =  function(){
       return "Hello "+this._name;
}

所以,总结一下:

  • 您可以使用经典的继承模式,它们对于共享数据类型很有用

  • 您还应该使用原型继承,它同样有效,并且在您想要共享功能的情况下更有效。

  • TheifMaster几乎做到了。在 JavaScript 中,私有私有确实不是什么大问题,只要您的代码定义了一个清晰的接口,这根本就不会有问题。我们都是在这里集中注意力的成年人:)

*聪明的读者可能会想:嗯?你不是在用历史规则欺骗我吗?我的意思是,属性访问没有被封装。

我说不,我不是。即使您没有明确地将字段封装为私有,您也可以简单地以不访问它们的方式定义您的合同。经常像 TheifMaster 建议使用_. 另外,我认为历史规则在很多这样的场景中并不是什么大问题,只要我们不改变属性访问处理父对象属性的方式。再次,这取决于我们。

getCar()是组合,而不是继承:-)
2021-03-26 22:03:21
本杰明,你的帖子简直太棒了。我自己说得再好不过了。我构建了 Aadit 非常讨厌的 jTypes 库,我认为您一针见血。经典继承只是一种设计模式,在这方面非常好。jTypes 只是为您将经典的类继承定义转换为原型链实例矩阵。它完成了在整个实例矩阵中移动的所有繁重工作,让您可以专注于类实现,同时仍然使用非常强大的语言(例如 JavaScript)工作。
2021-03-27 22:03:21
@Mene 当然!感谢您的关注,这就是我在 SO 编辑器中编写大量代码的结果:)
2021-03-31 22:03:21
我想指出在 js 中实现“类”的人经常忽略的一些东西:您正在限制代码的行为方式,就像任何形式的封装一样。js 的核心是原型的,所以你可以在其中实现类的事实表明前者更抽象,更强大(在经典语言中实现原型继承要困难得多)。Yegge 之前说过:steve-yegge.blogspot.com/2008/10/universal-design-pattern.html
2021-04-09 22:03:21
我现在在 JS 中开发了很多,我完全不知道你指出的语法糖。this在 JS 中的角色对我来说变得更加清晰了,谢谢!
2021-04-11 22:03:21

我不想气馁,因为您似乎是 StackOverflow 的一个相当新的成员,但是我将不得不面对您说尝试在 JavaScript 中实现经典继承是一个非常糟糕的主意.

注意:当我说在 JavaScript 中实现经典继承是一个坏主意时,我的意思是尝试在 JavaScript 中模拟实际的类、接口、访问修饰符等是一个坏主意。尽管如此,经典继承作为 JavaScript 中的一种设计模式还是很有用的,因为它只是原型继承的语法糖(例如最大最小类)。我一直在我的代码中使用这种设计模式(a la Augment)。

JavaScript 是一种原型的面向对象编程语言。不是经典的面向对象编程语言。当然,您可以在 JavaScript 之上实现经典继承,但在此之前请记住以下几点:

  1. 你违背了语言的精神,这意味着你将面临问题。很多问题——性能、可读性、可维护性等。
  2. 你不需要上课。托马斯,我知道你真的相信你需要课程,但请相信我。你没有。

为了你的缘故,我将为这个问题提供两个答案。第一个将向您展示如何在 JavaScript 中进行经典继承。第二个(我推荐)将教你接受原型继承。

JavaScript 中的经典继承

大多数程序员从尝试在 JavaScript 中实现经典继承开始。甚至像 Douglas Crockford 这样的 JavaScript 大师也尝试在 JavaScript 中实现经典继承我也尝试在 JavaScript 中实现经典继承。

首先,我创建了一个名为Clockwork的库,然后增加了. 但是,我不建议您使用这些库中的任何一个,因为它们违背了 JavaScript 的精神。事实是,当我编写这些经典继承库时,我还是一名业余 JavaScript 程序员。

我提到这一点的唯一原因是每个人在某个时间点都是业余爱好者,虽然我更希望您不要在 JavaScript 中使用经典继承模式,但我不能指望您能理解为什么原型继承很重要

如果不摔倒几次,您就无法学习如何骑自行车。我相信您仍处于原型继承的学习阶段。您对经典继承的需求就像自行车上的训练轮。

然而,训练轮很重要。如果你想要的话,那里有一些经典的继承库,它们应该会让你更舒服地用 JavaScript 编写代码。一个这样的库是jTypes请记住,当您对自己作为 JavaScript 程序员的技能充满信心时,请卸下训练轮。

注意:我个人一点也不喜欢 jTypes。

JavaScript 中的原型继承

我正在编写本节作为您的里程碑,以便您可以稍后回来并您准备好了解真正的原型继承知道下一步该做什么

首先,以下行是错误的:

基于 javascript 原型的面向对象编程风格很有趣,但在很多情况下,您需要能够从类创建对象。

这是错误的,因为:

  1. 您永远不需要从 JavaScript 中的类创建对象。
  2. 无法在 JavaScript 中创建类。

是的,可以在 JavaScript 中模拟经典继承。但是,您仍然是从对象而不是类继承属性。例如,ECMAScript Harmony 类只是原型继承的经典模式的语法糖。

在相同的上下文中,您给出的示例也是错误的:

例如,在矢量绘图应用程序中,工作区在绘图开始时通常是空的:我无法从现有的“线”创建新的“线”。更一般地说,动态创建对象的每种情况都需要使用类。

是的,即使工作区一开始是空的,您也可以从现有行创建新行。您需要了解的是,该线实际上并未绘制。

var line = {
    x1: 0,
    y1: 0,
    x2: 0,
    y2: 0,
    draw: function () {
        // drawing logic
    },
    create: function (x1, y1, x2, y2) {
        var line = Object.create(this);
        line.x1 = x1;
        line.y1 = y1;
        line.x2 = x2;
        line.y2 = y2;
        return line;
    }
};

现在您可以通过简单地调用来绘制上面的线,line.draw否则您可以从中创建一条新线:

var line2 = line.create(0, 0, 0, 100);
var line3 = line.create(0, 100, 100, 100);
var line4 = line.create(100, 100, 100, 0);
var line5 = line.create(100, 0, 0, 0);

line2.draw();
line3.draw();
line4.draw();
line5.draw();

绘制时线条line2形成一个正方形。line3line4line5100x100

结论

所以你看你真的不需要 JavaScript 中的类。对象就够了。使用函数可以很容易地实现封装。

也就是说,如果每个实例都没有自己的一组公共函数,就不能让每个实例的公共函数访问对象的私有状态。

然而,这不是问题,因为:

  1. 你真的不需要私有状态。你可能认为你有,但你真的没有。
  2. 如果您真的想将变量设为私有,那么正如ThiefMaster提到的,只需在变量名称前加上下划线,并告诉您的用户不要乱用它。

是的,这是我解决这个特定问题的尝试,尽管我认为遵循约定是一种更好的方法,即。_.前缀你的变量在这里,我只是跟踪数组中的实例,然后可以使用_destroy方法删除它们我相信这可以改进,但希望它会给你一些想法:

var Class = (function ClassModule() {

  var private = []; // array of objects of private variables

  function Class(value) {
    this._init();
    this._set('value', value);
  }

  Class.prototype = {

    // create new instance
    _init: function() {
      this.instance = private.length;
      private.push({ instance: this.instance });
    },

    // get private variable
    _get: function(prop) {
      return private[this.instance][prop];
    },

    // set private variable
    _set: function(prop, value) {
      return private[this.instance][prop] = value;
    },

    // remove private variables
    _destroy: function() {
      delete private[this.instance];
    },

    getValue: function() {
      return this._get('value');
    }
  };

  return Class;
}());

var a = new Class('foo');
var b = new Class('baz');

console.log(a.getValue()); //=> foo
console.log(b.getValue()); //=> baz

a._destroy();

console.log(b.getValue()); //=> baz
@Givi:不,我认为你需要多读一点。当你创建一个新实例时,它的__proto__属性被设置为引用与其内部原型(其构造函数的原型对象)相同的对象,没有重复,因为 JS 中的对象总是引用。
2021-03-16 22:03:21
@elclanrs - 假设我创建了一个实例a,然后创建了一个实例b然后,我打电话_destroya什么b.getValue()回报?看演示:jsfiddle.net/3B5c6你有问题。
2021-03-20 22:03:21
你实际上没有,因为它在原型中。
2021-03-29 22:03:21
@AaditMShah:检查编辑。一个快速的解决方案是只delete使用对象但保持索引完整。这不会太糟糕,大型数组查找很好,无论如何你可能不会有数千个实例(或者经常销毁它们......)
2021-04-05 22:03:21
@elclanrs - 无意冒犯,但您的解决方案似乎有点矫枉过正。是的 JavaScript 有它的局限性,但解决它们通常是不可行的,因为 JavaScript 从来没有被设计为可扩展的。与其尝试以您想要的方式使用 JavaScript,不如简单地使用 JavaScript。如果您真的想创建自己的领域特定语言,那么 LISP 是完美的选择。使用宏根据需要扩展它。这就是我喜欢 LISP 的原因。
2021-04-07 22:03:21

运行时不需要 private/public 这些是静态可执行的。任何足够复杂以强制执行不在外部使用的私有属性的项目都将有一个构建/预处理步骤,您可以使用它来验证事实。即使是具有 private/public 语法的语言也可以在运行时访问 private

至于定义基于类的对象,你使用的构造函数+原型是最简单、最有效的方式。任何类型的额外魔法都会更复杂且性能更差。

虽然你可以缓存prototype所以你不必一直重复ClassB.prototype.

//in node.js you can leave the wrapper function out
var ClassB = (function() {
    var method = ClassB.prototype;

    function ClassB( value ) {
        this._value = value;
    }

    method.getValue = function() {
        return this._value;
    };

    method.setValue = function( value ) {
        this._value = value;
    };

    return ClassB;
})();

以上不需要任何库,您可以轻松为其创建宏。

此外,在这种情况下,即使是正则表达式也足以验证“私有”属性是否被正确使用。运行/([a-zA-Z$_-][a-zA-Z0-9$_-]*)\._.+/g该文件并查看第一个匹配项始终是this. http://jsfiddle.net/7gumy/

据我所知,没有其他实例影响该值是不可能的,所以如果它是一个常量,你仍然可以将它包装在这样的函数中:

(function( context ) {

    'use strict';

    var SOME_CONSTANT = 'Hello World';

    var SomeObject = function() {};

    SomeObject.prototype.sayHello = function() {
        console.log(SOME_CONSTANT);
    };

    context.SomeObject = SomeObject;

})( this );

var someInstance = new SomeObject();
someInstance.sayHello();

您能做的最好的事情是使用下划线this._value代替 来注释不应触及属性this.value

请注意,私有函数可以通过将它们隐藏在函数中来实现:

(function( context ) {

    'use strict';

    var SomeObject = function() {};

    var getMessage = function() {
        return 'Hello World';
    };

    SomeObject.prototype.sayHello = function() {
        console.log(getMessage());
    };

    context.SomeObject = SomeObject;

})( this );

var someInstance = new SomeObject();
someInstance.sayHello();

这是 2 个“类”相互扩展和交互的示例:http : //jsfiddle.net/TV3H3/