在研究了使用JavaScript进行OOP的几种方法之后,我认为我终于想出了一种对我来说似乎不错的方法。可以吗您看到像这样在JavaScript中使用OOP会遇到的一些问题吗?

JSFiddle测试




 var Operators = (function ($, self) {
    var self = {};

    //private
    var IPT_X = '#x';
    var IPT_Y = '#y';

    //public
    self.x = 0;
    self.y = 0;

    //public
    self.showOperators = function() {
        //use of a private property (IPT_X) and a public property (this.x)
        $(IPT_X).val(self.x);
        $(IPT_Y).val(self.y);
    };

    self.clean = function() {
        self.x = 0;
        self.y = 0;
        // call to a local public method 
        self.showOperators();
    };

    self.updateOperators = function(_x, _y) {
        // use of a public property
        self.x = _x;
        self.y = _y;
    };

    self.clean();

    return self;

//Module dependencies
})(jQuery, {});

var Randomizer = (function(self, math) {

    // private
    function getRandomNumber() {
        return math.round(math.random() * 1000);
    };

    // keep superior method so we can call it after overriding 
    self.oldUpdateOperators = self.updateOperators

    // public
    self.updateOperators = function(_x, _y) {
        // call to superior class's method
        self.oldUpdateOperators(_x, _y);
        // call to method of superior object
        self.showOperators();
    };

    self.populateRandomNumbers = function() {
        // call to public local method (this.updateOperators())
        // and to a local private method (getRandomNumber()))
        self.updateOperators(getRandomNumber(), getRandomNumber());
    };

    // init
    self.populateRandomNumbers();

    return self;
})(Operators, Math)

var Operations = (function(self, $) {

    //private
    var IPT_RES = '#res';
    var BTN_SUM =  '#sum';
    var BTN_SUBTRACT =  '#subt';
    var BTN_MULTIPLY =  '#mult';
    var BTN_DIVISION =  '#div';
    var BTN_CLEAN =  '#clean';
    var BTN_RAND =  '#rand';

    // private
    function calcSum() {
        return self.x + self.y;
    };

    function calcSubtraction() {
        return self.x - self.y;
    };

    function calcMultiplication() {
        return self.x * self.y;
    };

    function calcDivision() {
        return self.x / self.y;
    };

    function showRes(val) {
        $(IPT_RES).val(val);
    };

    //public
    self.sum = function() {
        // call to 2 local private methods
        showRes(calcSum());
    };

    self.subtract = function() {
        showRes(calcSubtraction());
    };

    self.multiply = function() {
        showRes(calcMultiplication());
    };

    self.division = function() {
        showRes(calcDivision());
    }; 

    // init
    $(BTN_SUM).on('click', function() { self.sum() });
    $(BTN_SUBTRACT).on('click', function() { self.subtract() });
    $(BTN_MULTIPLY).on('click', function() { self.multiply() });
    $(BTN_DIVISION).on('click', function() { self.division() });   
    $(BTN_CLEAN).on('click', function() { self.clean() });
    $(BTN_RAND).on('click', function() { self.populateRandomNumbers() });

    return self;

})(Randomizer, jQuery); 

 <html>
<body>
X: <input id='x'>
<br>
Y: <input id='y'>
<br>
Res: <input id='res'>
<br>
<input id='sum' type='button' value='+'>
<input id='subt' type='button' value='-'>
<input id='mult' type='button' value='*'>
<input id='div' type='button' value='/'>
<input id='clean' type='button' value='C'>
<input id='rand' type='button' value='Rand'>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js"></script>

</body>
</html> 





优点:


清洁代码。没有太多样板代码
无需使用.call.bind,在我看来,这会使代码更难以理解
清晰定义的继承
无需实例化任何内容(新)。类定义的简单存在就足以使一切正常工作。
一旦不需要在html中定义任何内容,就可以在视图和业务层上实现清晰的分离。所有动作都绑定在JavaScript中
不用担心原型
我已经在生产系统中使用了这种方法,并且可以正常工作。

缺点:


我无法调用更高版本的方法。我的意思是,调用已经被覆盖的“祖父”类的方法。仅适用于最后一个方法版本。我很少这样做,所以我认为还可以。
与JavaScript中通常所做的有很大不同。虽然,我对此真的不确定。

这个问题的目的不是简单计算器的代码审查。我是一名高级开发人员,现在已经学习JS一段时间,并且使用其他几种语言和FW进行开发。您可以在我的博客文章此处查看以前研究过的其他(更详细的)OOP方法。

我非常清楚MVC的工作原理以及每一层的功能。我已经做了一个简单的计算器作为示例,以说明我打算如何进行JS OOP。我将UI处理代码和逻辑混合在一起只是为了简化示例。在真实的系统中,我会将其分为不同的类。名称也是如此。在实际代码中,我总是使用非常冗长的名称。

我试图在JS中简化OO,并创建了此技术。正如我所说的,它已经在生产系统中使用过。

这个问题的目的是将这种OOP格式提供给其他开发人员,以查看是否有我看不到的任何问题。所以我不担心这个特定的任务,因为它仅仅是一个例子。为了重点解决这一问题,请坚持在OOP中执行此操作。

如果有人认为还可以,我也希望收到您的来信。

编辑

我通过许多建议的修改创建了一个新问题,在这里:简单的面向对象计算器-后续操作

#1 楼

私有变量

我看到了您提供的用于表示哪些变量是公共变量或私有变量的注释:


//private
var IPT_X = '#x';
var IPT_Y = '#y';

//public
self.x = 0;
self.y = 0;



但是这些评论并不太有用。如果您想要一种更好的方法(更标准的是),可以在变量名称之前用下划线表示私有变量。看起来像这样:

var _IPT_X = '#x';
var _IPT_Y = '#y';

self.x = 0;
self.y = 0;




通过查看代码,您正在遵循类似Python的语法。为了保持一致,我将始终将self作为第一个参数。


var Operators = (function ($, self) {



更改为...

var Operators = (function (self, $) {



重新创建JQuery对象

正如@YiJiang在问题注释中所说,存储JQuery对象比不断调用$更好。关于您拥有的私有变量。因此,您可以执行以下操作:


var IPT_X = '#x';



更改为...

var _$IPT_X = $('#x');



存储过去的内容

在以下代码中:


// keep superior method so we can call it after overriding 
self.oldUpdateOperators = self.updateOperators



I感觉似乎更好的方法来覆盖函数是在self.updateOperators上放置一些称为override的属性。

function overridable(self, prop, descriptor) {
    Object.defineProperty(self, prop, descriptor);
    // If you cannot write over the property then there cannot be an override.
    if(!descriptor.writable) return self;

    // This is the function that will be used to override the property.
    Object.defineProperty(self[prop], "override", { value: function (descriptor) {
        // Creates a chain of overridden properties that store the old property values.
        var overridden = self[prop];
        overridable(self, prop, descriptor);
        self[prop].overridden = overridden;
    }});
    return self;
}


这里是如何使用函数:

var obj = {};
overridable(obj, "add", { value: function(a,b) { return a + b }, writable: true });
obj.add(1,2); // => 3
obj.add.override({ value: function(a,b,c) { return a + b + c }, writable: true });
obj.add(1,2,3);            // => 6
obj.add.overridden(1,2,3); // => 3
obj.add.override({ value: function(a,b,c,d) { return a + b + c + d }, writable: true });
obj.add(1,2,3,4);                       // => 10
obj.add.overridden(1,2,3,4);            // => 6
obj.add.overridden.overridden(1,2,3,4); // => 3


注意:绝对不是防弹。因此,请尝试使用它以使其适合您的需求。

有关Object.defineProperty的更多信息。


要添加的内容

我发现用JavaScript编写库时将所有内容放在一个大函数中来控制范围非常有用。如下所示:

(function(scope) {
/* Library */
})(this)


这可以防止泛洪全局范围,并且您可以控制库连接到的范围。

这是一个例如:

(function(scope, dp) {

var _foo = "bar!";
function _f() { return _foo };

var lib = { bar: _f };

// Making to where it is a non-enumerable, non-configurable, and non-writable property to keep the version stuck there.
dp(lib, "VERSION", { value: "1.0.0" });

dp(scope, "lib", { value: lib });

})(this, Object.defineProperty)


这样,我可以大量使用Object.defineProperty之类的东西而不会为所添加的字符感到不适。


其他

我觉得您应该看一些著名的库,看看它们如何处理大量代码。然后,尝试将他们的一些技巧带入您的风格,因为它们已经存在了一段时间,并且一定有其作用的原因。
我知道一些“大牌”:

jquery.js(当然),underscore.jsbackbone.js。然后有一些我喜欢的东西:Three.jsTwo.js

还有很多,但我现在能想到的就是这些。


您的风格

我个人的观点是,由于JavaScript是一种脚本语言,所以我认为您可以根据自己的建筑风格来开发自己的风格。您的样式对于您一直在编写的代码很有用,但是我永远不会以一种特定的方式进行设置。可以做一些事情,例如使用要求self作为第一个参数的声明来模仿Python语法,但是您应该始终根据所编写的代码来塑造样式。


抱歉,发布此消息花了一段时间,我整天不停地工作。希望这会有所帮助!

评论


\ $ \ begingroup \ $
很有帮助! :D我发现Objet.defineProperty特别有趣,并将对其进行更深入的研究。这是我的技术缺乏的东西。在层次结构链中达到更高地位的一种方法。谢谢!仅作记录,我不打算使用python语法。我来自Pascal / C ++ / Java / PHP背景。我仍然维护一个Python项目。我使用self是因为我不能在语法中使用它。所以我不得不选择一些东西,自我对我来说似乎是正确的选择。但这不是一个封闭的问题。我可以使用“ ths”或“ this_”。你认为呢 ?
\ $ \ endgroup \ $
–尼尔森·特谢拉(Nelson Teixeira)
15年12月24日在23:55

\ $ \ begingroup \ $
您会推荐哪些“大牌”图书馆?我现在可以想到prototype.js。任何其他 ?
\ $ \ endgroup \ $
–尼尔森·特谢拉(Nelson Teixeira)
15年12月24日在23:56

\ $ \ begingroup \ $
还有一个问题:我最想知道的是,这种操作方式是否存在一些严重的缺陷。根据您的回答并考虑您的建议修改,我认为您基本上可以找到它。我对吗 ?
\ $ \ endgroup \ $
–尼尔森·特谢拉(Nelson Teixeira)
2015年12月24日23:58



\ $ \ begingroup \ $
@NelsonTeixeira。自我是一个很好的选择,我自己使用。 (或者我在我懒惰的时候笑)。它让我想起了Python。没有“关键缺陷”,我认为您应该更改样式以适合您要实现的目标。由于您使它看起来更像是面向对象的,对我来说,这很好。我仍然会添加我正在谈论的范围界定,以保护全球范围。您可以做的是建立自己的模拟OOP的库,以了解有关JavaScript的更多信息。 (写方法如可重写)
\ $ \ endgroup \ $
–tkellehe
2015年12月25日,0:29

\ $ \ begingroup \ $
@NelsonTeixeira。听起来不错!如果可以的话,我想最终看看您最终会做什么:)
\ $ \ endgroup \ $
–tkellehe
2015年12月25日下午2:27

#2 楼

首先,我想说jQuery的存在对于您完成的任务来说是过大的。可以使用Vanilla DOM处理。此外,您似乎只使用jQuery处理事件和设置值,而addEventListenerinput.value无法处理。

接下来是将UI代码从逻辑上分离,分离关注点。在这里,您的操作模块似乎知道DOM。您的操作员也知道,但是为什么呢?随机还知道您要更新运算符,但随机不仅仅是为您生成随机数吗?

要可视化,它应该类似于

    -- events ->                  -- data -->        
DOM              UI handling code              logic <-> utils
    <- results --                 <- results --


UI处理代码将处理DOM事件和操作。只有在这里,我们才能看到与DOM相关的代码。逻辑将仅处理业务逻辑,例如特定于应用程序的内容。实用程序(这是一个糟糕的名称)通常是可以帮助您提高逻辑性的库和代码片段,但不一定绑定到您的应用程序(例如您的随机数生成器)。

要注意的另一件事是您的名称。我们使用的是一种非常高级的语言,这与空间无关(存在缩小器)。详细命名您的内容。 xy可以表示任何含义,value1value2怎么样。您拥有选择器的“常量”并没有被命名为包含选择器,但看起来它们实际上包含了对DOM元素的引用(BTN_CLEAN,但这并不是一个真正的按钮)。

这是一个超级选择它的简化版本:




 // This part can live in another file, and are only about operations.
// No part of this code is aware of the DOM.

var CalculatorOperations = {
  add: function(value1, value2) {
    return value1 + value2;
  },
  subtract: function(value1, value2) {
    return value1 - value2;
  },
  multiply: function(value1, value2) {
    return value1 * value2;
  },
  divide: function(value1, value2) {
    return value1 / value2;
  },
  generateRandomNumber: function() {
    return (Math.random() * 1000) | 0;
  },
};

// This part manages events and *uses* the operations.
// No part of this code actually does the logic.

(function() {

  var input1 = document.getElementById('input-1');
  var input2 = document.getElementById('input-2');
  var result = document.getElementById('result');
  var addButton = document.getElementById('add-button');
  var subtractButton = document.getElementById('subtract-button');
  var multiplyButton = document.getElementById('multiply-button');
  var divideButton = document.getElementById('divide-button');
  var clearButton = document.getElementById('clear-button');
  var randomButton = document.getElementById('random-button');

  function setResult(value) {
    result.value = value;
  }

  function setValue1(value) {
    input1.value = value;
  }

  function setValue2(value) {
    input2.value = value;
  }

  addButton.addEventListener('click', function() {
    setResult(CalculatorOperations.add(+input1.value, +input2.value));
  });

  subtractButton.addEventListener('click', function() {
    setResult(CalculatorOperations.subtract(+input1.value, +input2.value));
  });

  multiplyButton.addEventListener('click', function() {
    setResult(CalculatorOperations.multiply(+input1.value, +input2.value));
  });

  divideButton.addEventListener('click', function() {
    setResult(CalculatorOperations.divide(+input1.value, +input2.value));
  });

  clearButton.addEventListener('click', function() {
    setValue1('');
    setValue2('');
    setResult('');
  });

  randomButton.addEventListener('click', function() {
    setValue1(CalculatorOperations.generateRandomNumber());
    setValue2(CalculatorOperations.generateRandomNumber());
  });
}()); 

 X:
<input id='input-1'>
<br>Y:
<input id='input-2'>
<br>Res:
<input id='result'>
<br>
<input id='add-button' type='button' value='+'>
<input id='subtract-button' type='button' value='-'>
<input id='multiply-button' type='button' value='*'>
<input id='divide-button' type='button' value='/'>
<input id='clear-button' type='button' value='C'>
<input id='random-button' type='button' value='Rand'> 





OOP不是“必须的”。这只是JavaScript中可用的范例之一。尽管“必须”是使您的代码简单易维护。在您的版本中,我到处跑是因为一些代码做到了这一点,而另一部分又做到了。在上面的代码中,我知道与DOM相关的代码在哪里发生,逻辑在哪里发生,谁在做什么。

评论


\ $ \ begingroup \ $
您的代码不正确。对于55 + 65,它将返回5565。您应该使用+ val1 + + val2或parseInt(val1,10)+ parseInt(val2,10)
\ $ \ endgroup \ $
–伊斯梅尔·米格尔(Ismael Miguel)
15年12月24日在21:07

\ $ \ begingroup \ $
同意约瑟夫,最近,我开始使用+ val1 + + val2来确保将字符串类型的数字转换为数字。除非我也想截断小数,否则我将远离parseInt。我不喜欢parseInt的另一件事是,它忽略字符串中数字之后的非数字。
\ $ \ endgroup \ $
–将
2015年12月25日在5:10

\ $ \ begingroup \ $
不错的评论。我还有2个附加功能:a)您可以节省大量时间来编写快捷方式$ = function(el){return document.querySelectorAll(el)} b)您可以使用委托事件侦听器:给按钮id如“ add ”,“减”,您可以根据名称切换到计算器操作
\ $ \ endgroup \ $
– Thomas垃圾
2015年12月25日在9:03

#3 楼

您拥有的内容更接近装饰器模式:您正在用附加功能装饰基础对象。它不是完全继承。它也不是完全“类”,因为它实际上只是一个对象实例。

您的结构导致一个主要问题:您的3个“类”是完全相同的对象。您只有3个变量对其进行引用。

此外,由于您正在使用IIFE来定义它们中的每一个,因此您无法再次实例化/创建某些东西。一旦被评估,装饰器函数将丢失,仅保留装饰的对象。无论哪种方式,都不是典型的面向对象设计。

它也不是通常的装饰器模式。真正的装饰器函数将是通用的,并且适用于多种类型的对象(例如,请参见prototype.js;它基本上通过方法的组合来装饰不同的本机浏览器对象),但是在这里,这更像是将代码划分为命名的“步骤”。虽然有用,但也可以通过代码注释来完成。

函数之间的耦合也困扰着我。您可以进行一些轻度的重构,从而简单地删除每个调用,因此可以重复使用它们,例如:

转而得到一个合理有用的对象,因为每个对象都依赖于前一个对象。当然,这是子类的工作方式,但是由于这不是完全子类,因此您必须自己进行管理。即唯一真正有用的调用是Operations(Randomizer(Operators(x)))。之所以能够满足这种耦合,是因为您使用了IIFE,并确保只用一次“正确的方式”进行调用。

我将通过在函数中明确依赖项链来对此进行更改:

var Operators = function ($, self) { ... };
var Randomizer = function (self, math) { ... };
var Operations = function (self, $) { ... };


现在,它更像是典型的“类”:


您可以调用任何“链中的链接”,并且它请确保其“祖先”被调用。
您可以从任何步骤中获得一个“实例”。它们不仅是一次IIFE。

如果您有要扩展的兼容对象,则可以将它们用作常规装饰器。

仍然有足够的耦合此处(也针对页面上的UI元素),这不是最直接的构造。它仍然闻起来像以复杂的方式将代码块分成多个单独的块。

总而言之:当然可以,并且可读性强。但这也有点奇怪。

其他说明


Operators应该命名为Operands。运算符是+-等。操作数是像xy这样的运算符所要操作的值。
与您的参数一致。 Operatorsself作为其第二个参数,而其他两个将self作为第一个参数。容易弄乱。

不要传递不需要的东西。 Math是全局对象,因此将其作为名为math的参数传递实际上并没有任何用处。
jQuery传递为$更有意义,因为这意味着即使在全局范围内使用$,也可以使用jQuery.noConflict()别名。但是,由于您可能想对所有函数都使用它,因此我将执行以下操作:

var Operators = function ($, self) {
  self = self || {};
  // ...
  return self;
};

var Randomizer = function (self, math) {
  self = Operators(self);
  // ...
  return self;
};

var Operations = function (self, $) {
  self = Randomizer(self);
  // ...
  return self;
};


您实际上无法以以下形式输入自己的值。或:可以,但不会改变结果。视图中的更改不会反映在对象的状态中。只是相反。即使这样,也只有在您致电showOperators(哼,“ showOperands”)时才这样做。因此,恐怕它作为计算器并不是很有用。


评论


\ $ \ begingroup \ $
哇...你们摇滚。我真的很喜欢这个网站。只有爱,没有苛刻的评论。与我们Stackoverflow的正常工作有何不同。 :D那是一个很好的批评和建议。明天我将尝试您建议的修改,然后再回头。非常感谢 :)
\ $ \ endgroup \ $
–尼尔森·特谢拉(Nelson Teixeira)
2015年12月25日4:44



\ $ \ begingroup \ $
根据答案对我进行了许多修改,对问题进行了修改。 :)
\ $ \ endgroup \ $
–尼尔森·特谢拉(Nelson Teixeira)
2015年12月26日下午3:43

\ $ \ begingroup \ $
@NelsonTeixeira一旦被回答,请不要编辑问题。它使我们所有的答案都是无效和无关紧要的。 Q&A格式取决于匹配Q。如果您有新代码,请张贴新问题,然后获得新答案。我会自由地撤消您的编辑(除非在我写作时有人殴打我)
\ $ \ endgroup \ $
– Flaambino
15/12/26在3:47

\ $ \ begingroup \ $
好的...那我要提一个新问题
\ $ \ endgroup \ $
–尼尔森·特谢拉(Nelson Teixeira)
2015年12月26日,下午3:50

\ $ \ begingroup \ $
@NelsonTeixeira Nope-实际上,这是值得鼓励的:)因此,一定要将两者联系起来!您也可以使用新问题的链接来编辑此问题。关键是不要对现有问题的主要内容进行太多修改,以使答案不同步,但是链接到后续问题就可以了。
\ $ \ endgroup \ $
– Flaambino
15/12/26在3:57