Classes vs. Objects

我们已经看到了各种理论的探索和“类”与“行为委托”的思维模型的比较。现在让我们来看看更具体的代码场景,来展示你如何实际应用这些想法。

我们将首先讲解一种在前端网页开发中的典型场景:建造 UI 部件(按钮,下拉列表等等)。

Widget “类”

因为你可能还是如此地习惯于 OO 设计模式,你很可能会立即这样考虑这个问题:一个父类(也许称为 Widget)拥有所有共通的基本部件行为,然后衍生的子类拥有具体的部件类型(比如 Button)。

注意: 为了 DOM 和 CSS 的操作,我们将在这里使用 JQuery,这仅仅是因为对于我们现在的讨论,它不是一个我们真正关心的细节。这些代码中不关心你用哪个 JS 框架(JQuery,Dojo,YUI 等等)来解决如此无趣的问题。

让我们来看看,在没有任何“类”帮助库或语法的情况下,我们如何用经典风格的纯 JS 来实现“类”设计:

  1. // 父类
  2. function Widget(width,height) {
  3. this.width = width || 50;
  4. this.height = height || 50;
  5. this.$elem = null;
  6. }
  7. Widget.prototype.render = function($where){
  8. if (this.$elem) {
  9. this.$elem.css( {
  10. width: this.width + "px",
  11. height: this.height + "px"
  12. } ).appendTo( $where );
  13. }
  14. };
  15. // 子类
  16. function Button(width,height,label) {
  17. // "super"构造器调用
  18. Widget.call( this, width, height );
  19. this.label = label || "Default";
  20. this.$elem = $( "<button>" ).text( this.label );
  21. }
  22. // 使 `Button` “继承” `Widget`
  23. Button.prototype = Object.create( Widget.prototype );
  24. // 覆盖“继承来的” `render(..)`
  25. Button.prototype.render = function($where) {
  26. // "super"调用
  27. Widget.prototype.render.call( this, $where );
  28. this.$elem.click( this.onClick.bind( this ) );
  29. };
  30. Button.prototype.onClick = function(evt) {
  31. console.log( "Button '" + this.label + "' clicked!" );
  32. };
  33. $( document ).ready( function(){
  34. var $body = $( document.body );
  35. var btn1 = new Button( 125, 30, "Hello" );
  36. var btn2 = new Button( 150, 40, "World" );
  37. btn1.render( $body );
  38. btn2.render( $body );
  39. } );

OO 设计模式告诉我们要在父类中声明一个基础 render(..),之后在我们的子类中覆盖它,但不是完全替代它,而是用按钮特定的行为增强这个基础功能。

注意 显式假想多态 的丑态,Widget.callWidget.prototype.render.call 引用是为了伪装从子“类”方法得到“父类”基础方法支持的“super”调用。呃。

ES6 class 语法糖

我们会在附录A中讲解 ES6 的 class 语法糖,但是让我们演示一下我们如何用 class 来实现相同的代码。

  1. class Widget {
  2. constructor(width,height) {
  3. this.width = width || 50;
  4. this.height = height || 50;
  5. this.$elem = null;
  6. }
  7. render($where){
  8. if (this.$elem) {
  9. this.$elem.css( {
  10. width: this.width + "px",
  11. height: this.height + "px"
  12. } ).appendTo( $where );
  13. }
  14. }
  15. }
  16. class Button extends Widget {
  17. constructor(width,height,label) {
  18. super( width, height );
  19. this.label = label || "Default";
  20. this.$elem = $( "<button>" ).text( this.label );
  21. }
  22. render($where) {
  23. super.render( $where );
  24. this.$elem.click( this.onClick.bind( this ) );
  25. }
  26. onClick(evt) {
  27. console.log( "Button '" + this.label + "' clicked!" );
  28. }
  29. }
  30. $( document ).ready( function(){
  31. var $body = $( document.body );
  32. var btn1 = new Button( 125, 30, "Hello" );
  33. var btn2 = new Button( 150, 40, "World" );
  34. btn1.render( $body );
  35. btn2.render( $body );
  36. } );

毋庸置疑,通过使用 ES6 的 class,许多前面经典方法中难看的语法被改善了。super(..) 的存在看起来非常适宜(但当你深入挖掘它时,不全是好事!)。

除了语法上的改进,这些都不是 真正的,因为它们仍然工作在 [[Prototype]] 机制之上。它们依然会受到思维模型不匹配的拖累,就像我们在第四,五章中,和直到现在探索的那样。附录A将会详细讲解 ES6 class 语法和它的含义。我们将会看到为什么解决语法上的小问题不会实质上解决我们在 JS 中的类的困惑,虽然它做出了勇敢的努力假装解决了问题!

无论你是使用经典的原型语法还是新的 ES6 语法糖,你依然选择了使用“类”来对问题(UI 部件)进行建模。正如我们前面几章试着展示的,在 JavaScript 中做这个选择会带给你额外的头疼和思维上的弯路。

委托部件对象

这是我们更简单的 Widget/Button 例子,使用了 OLOO 风格委托

  1. var Widget = {
  2. init: function(width,height){
  3. this.width = width || 50;
  4. this.height = height || 50;
  5. this.$elem = null;
  6. },
  7. insert: function($where){
  8. if (this.$elem) {
  9. this.$elem.css( {
  10. width: this.width + "px",
  11. height: this.height + "px"
  12. } ).appendTo( $where );
  13. }
  14. }
  15. };
  16. var Button = Object.create( Widget );
  17. Button.setup = function(width,height,label){
  18. // delegated call
  19. this.init( width, height );
  20. this.label = label || "Default";
  21. this.$elem = $( "<button>" ).text( this.label );
  22. };
  23. Button.build = function($where) {
  24. // delegated call
  25. this.insert( $where );
  26. this.$elem.click( this.onClick.bind( this ) );
  27. };
  28. Button.onClick = function(evt) {
  29. console.log( "Button '" + this.label + "' clicked!" );
  30. };
  31. $( document ).ready( function(){
  32. var $body = $( document.body );
  33. var btn1 = Object.create( Button );
  34. btn1.setup( 125, 30, "Hello" );
  35. var btn2 = Object.create( Button );
  36. btn2.setup( 150, 40, "World" );
  37. btn1.build( $body );
  38. btn2.build( $body );
  39. } );

使用这种 OLOO 风格的方法,我们不认为 Widget 是一个父类而 Button 是一个子类,Widget 只是一个对象 和某种具体类型的部件也许想要代理到的工具的集合,而且 Button 也只是一个独立的对象(当然,带有委托至 Widget 的链接!)。

从设计模式的角度来看,我们 没有 像类的方法建议的那样,在两个对象中共享相同的 render(..) 方法名称,而是选择了更能描述每个特定任务的不同的名称。同样的原因,初始化 方法被分别称为 init(..)setup(..)

不仅委托设计模式建议使用不同而且更具描述性的名称,而且在 OLOO 中这样做会避免难看的显式假想多态调用,正如你可以通过简单,相对的 this.init(..)this.insert(..) 委托调用看到的。

语法上,我们也没有任何构造器,.prototype 或者 new 出现,它们事实上是不必要的设计。

现在,如果你再细心考察一下,你可能会注意到之前仅有一个调用(var btn1 = new Button(..)),而现在有了两个(var btn1 = Object.create(Button)btn1.setup(..))。这猛地看起来像是一个缺点(代码变多了)。

然而,即便是这样的事情,和经典原型风格比起来也是 OLOO 风格代码的优点。为什么?

用类的构造器,你“强制”(不完全是这样,但是被强烈建议)构建和初始化在同一个步骤中进行。然而,有许多种情况,能够将这两步分开做(就像你在 OLOO 中做的)更灵活。

举个例子,我们假定你在程序的最开始,在一个池中创建所有的实例,但你等到在它们被从池中找出并使用之前再用指定的设置初始化它们。我们的例子中,这两个调用紧挨在一起,当然它们也可以按需要发生在非常不同的时间和代码中非常不同的部分。

OLOO 对关注点分离原则有 更好 的支持,也就是创建和初始化没有必要合并在同一个操作中。