【问题标题】:Kyle Simpson's OLOO Pattern vs Prototype Design PatternKyle Simpson 的 OLOO 模式与原型设计模式
【发布时间】:2015-06-29 13:22:11
【问题描述】:

Kyle Simpson 的“OLOO(链接到其他对象的对象)模式”与原型设计模式有什么不同吗?除了通过专门表示“链接”(原型的行为)的东西来创造它并澄清这里不会发生“复制”(类的行为)之外,他的模式究竟引入了什么?

这是他的书中的an example of Kyle's pattern,“你不知道 JS:this & Object Prototypes”:

var Foo = {
    init: function(who) {
        this.me = who;
    },
    identify: function() {
        return "I am " + this.me;
    }
};

var Bar = Object.create(Foo);

Bar.speak = function() {
    alert("Hello, " + this.identify() + ".");
};

var b1 = Object.create(Bar);
b1.init("b1");
var b2 = Object.create(Bar);
b2.init("b2");

b1.speak(); // alerts: "Hello, I am b1."
b2.speak(); // alerts: "Hello, I am b2."

【问题讨论】:

  • 您能否至少链接到您所询问的模式的描述。更好的是在您的问题中显示它的代码示例。
  • Getify 有时在 Stackoverflow 上。我已经在推特上给他发了这个问题:)

标签: javascript design-patterns


【解决方案1】:

他的模式究竟介绍了什么?

OLOO 按原样包含原型链,无需叠加其他(IMO 混淆)语义来获得链接。

因此,这两个 sn-ps 具有完全相同的结果,但实现方式不同。

构造函数形式:

function Foo() {}
Foo.prototype.y = 11;

function Bar() {}
Bar.prototype = Object.create(Foo.prototype);
Bar.prototype.z = 31;

var x = new Bar();
x.y + x.z;  // 42

OLOO 表单:

var FooObj = { y: 11 };

var BarObj = Object.create(FooObj);
BarObj.z = 31;

var x = Object.create(BarObj);
x.y + x.z;  // 42

在两个 sn-ps 中,x 对象是 [[Prototype]]-链接到一个对象(Bar.prototype 或 BarObj),而该对象又链接到第三个对象(Foo.prototype 或 FooObj) .

sn-ps 之间的关系和委托是相同的。 sn-ps 之间的内存使用情况相同。在 sn-ps 之间创建许多“孩子”(也就是 x1 到 x1000 等许多对象)的能力是相同的。委托的性能(x.y 和 x.z)在 sn-ps 之间是相同的。 OLOO 的对象创建性能较慢,但sanity checking that 表明性能较慢确实不是问题。

我认为 OLOO 提供的是,仅表达对象并直接链接它们比通过构造函数/new 机制间接链接它们要简单得多。后者假装是关于类的,但实际上只是一种用于表达委托的糟糕语法(旁注: ES6 class 语法也是如此!)。

OLOO 只是去掉了中间人。

这是another comparison 中的class vs OLOO。

【讨论】:

  • 我发现你的回答和书中描述的 OLOO 想法真的很有趣,我希望得到你对这个问题的反馈:stackoverflow.com/questions/40395762/… 特别是如果你发现这个实现是正确的,以及如何解决与问题相关的问题访问私有成员。感谢您提前抽出时间,并祝贺您的​​最新书籍。
  • 但是Object.create(...) 比new 慢很多倍。 jsperf.com/object-create-vs-crockford-vs-jorge-vs-constructor/…
  • @Pier 性能实际上并不是什么大问题。修复了关于健全性检查对象创建性能的损坏的博客文章链接,该链接解释了如何正确考虑这一点。
  • 而且 jQuery 比 DOM API 慢,对吧?但是,这是今年,伙计——我宁愿写得优雅而简单,也不愿担心优化。以后如果需要微优化,到时候再操心。
  • 我现在想补充一点,仅仅一年多之后,Object.create() 在 chrome 中得到了高度优化,jsperf 显示了它——它是现在最快的选项之一。这恰恰说明了为什么您不应该关注这种微优化,而只需编写算法合理的代码。
【解决方案2】:

我阅读了 Kyle 的书,发现它的内容非常丰富,尤其是关于 this 绑定方式的详细信息。

优点:

对我来说,OLOO 有几大优点:

1。简单

OLOO 依赖Object.create() 创建一个新对象,该对象是[[prototype]]-链接到另一个对象。您不必了解函数具有 prototype 属性,也不必担心其修改可能带来的任何相关缺陷。

2。更简洁的语法

这是有争议的,但我觉得 OLOO 语法(在许多情况下)比“标准”javascript 方法更简洁、更简洁,尤其是在多态性方面(super 风格的调用)。

缺点:

我认为有一点设计有问题(实际上有助于上述第 2 点),那就是与阴影有关:

在行为委托中,我们尽可能避免在[[Prototype]] 链的不同级别上将事物命名为相同。

这背后的想法是,对象有自己的更具体的功能,然后在内部委托给链下游的功能。例如,您可能有一个带有save() 函数的resource 对象,它将对象的JSON 版本发送到服务器,但您也可能有一个带有stripAndSave() 函数的clientResource 对象,它首先删除不应发送到服务器的属性。

潜在的问题是:如果其他人出现并决定创建一个 specialResource 对象,但不完全了解整个原型链,他们可能会合理地*决定在名为 @ 的属性下保存最后一次保存的时间戳987654333@,它隐藏了resource 对象上的基本save() 功能,沿着原型链向下链接了两个链接:

var resource = {
  save: function () { 
    console.log('Saving');
  }
};

var clientResource = Object.create(resource);

clientResource.stripAndSave = function () {
  // Do something else, then delegate
  console.log('Stripping unwanted properties');
  this.save();
};

var specialResource = Object.create( clientResource );

specialResource.timeStampedSave = function () {
  // Set the timestamp of the last save
  this.save = Date.now();
  this.stripAndSave();
};

a = Object.create(clientResource);
b = Object.create(specialResource);

a.stripAndSave();    // "Stripping unwanted properties" & "Saving".
b.timeStampedSave(); // Error!

这是一个特别人为的例子,但重点是不特别遮蔽其他属性会导致一些尴尬的情况和大量使用同义词库!

init 方法也许更好地说明了这一点 - 特别是因为 OOLO 回避了构造函数类型函数。由于每个相关对象都可能需要这样的函数,因此恰当地命名它们可能是一项乏味的练习,而且其唯一性可能会让人难以记住要使用哪个。

*其实也不是特别合理(lastSaved会好很多,不过只是个例子)

【讨论】:

  • 我同意名称冲突的可能性是一个缺点......但实际上这是 [[Prototype]] 系统本身的缺点,而不是专门的 OLOO。
  • 也许这本书也应该提到?
  • 我不确定这是否真的是@Ed Hinchliffe 描述的问题的解决方案,因为它只是将 save() 移动到它自己的命名空间,但它确实有效codepen.io/tforward/pen/govEPr?editors=1010
  • 我认为 @ed-hinchliffe 在代码 sn-p 的最后一行是 b.timeStampedSave(); 而不是 a.timeStampedSave();。
  • @tristan-forward 感谢您将瑞克和莫蒂带入其中!
【解决方案3】:

“你不知道 JS:this & Object Prototypes”中的讨论和 OLOO 的介绍发人深省,我通过这本书学到了很多东西。 OLOO 模式的优点在其他答案中得到了很好的描述;但是,我对它有以下宠物投诉(或者我遗漏了一些阻止我有效应用它的东西):

1

当一个“类”在经典模式中“继承”另一个“类”时,这两个函数可以声明为相似的语法("function declaration" or "function statement"):

function Point(x,y) {
    this.x = x;
    this.y = y;
};

function Point3D(x,y,z) {
    Point.call(this, x,y);
    this.z = z;
};

Point3D.prototype = Object.create(Point.prototype);

相比之下,在 OLOO 模式中,用于定义基础对象和派生对象的不同语法形式:

var Point = {
    init  : function(x,y) {
        this.x = x;
        this.y = y;
    }
};


var Point3D = Object.create(Point);
Point3D.init = function(x,y,z) {
    Point.init.call(this, x, y);
    this.z = z;
};

正如您在上面的示例中看到的,基础对象可以使用对象文字表示法定义,而相同的表示法不能用于派生对象。这种不对称让我很烦。

2

在OLOO模式中,创建一个对象分为两步:

  1. 致电Object.create
  2. 调用一些自定义的非标准方法来初始化对象(您必须记住,因为它可能因对象而异):

     var p2a = Object.create(Point);
    
     p2a.init(1,1);
    

相比之下,在原型模式中,您使用标准运算符new:

var p2a = new Point(1,1);

3

在经典模式中,我可以通过将它们直接分配给“类”函数(与其.prototype相反)来创建不直接应用于“即时”的“静态”实用程序函数。例如。喜欢下面代码中的函数square:

Point.square = function(x) {return x*x;};

Point.prototype.length = function() {
    return Math.sqrt(Point.square(this.x)+Point.square(this.y));
};

相比之下,在 OLOO 模式中,对象实例上也可以使用任何“静态”函数(通过 [[prototype]] 链):

var Point = {
    init  : function(x,y) {
        this.x = x;
        this.y = y;
    },
    square: function(x) {return x*x;},
    length: function() {return Math.sqrt(Point.square(this.x)+Point.square(this.y));}
};

【讨论】:

  • 第一个代码示例中没有文字。您可能误用了“字面”一词,赋予了它另一种含义。只是说...
  • 关于第二点,作者认为将创建和初始化分开是一种“更好”的关注点分离,并指出可能存在一些罕见的用例可能会发光(例如对象池)。我觉得这个论点非常弱。
  • 再次关于第二点,使用 OLOO,您可以一次创建对象,然后等待初始化,而使用构造函数,您必须在创建时进行初始化,因此 Kyle 认为这是一个好处。
  • @IvanKleshnin “对象文字是用大括号括起来的名称-值对的逗号分隔列表”。在第一个示例的第二部分中分配给 var Point 的对象 {init: function(x,y) ..} 是我书中的对象文字。
【解决方案4】:

“我想这样做会使每个 obj 依赖于另一个”

正如 Kyle 所解释的,当两个对象被 [[Prototype]] 链接时,它们并不是真的 相互依赖;相反,它们是单独的对象。你正在链接一个 使用[[Prototype]] 链接反对对方,您可以随时更改。 如果您将通过 OLOO 样式创建的两个 [[Prototype]] 链接对象视为相互依赖,那么您也应该对通过 constructor 调用创建的对象进行同样的思考。

var foo= {},
    bar= Object.create(foo),
    baz= Object.create(bar);


console.log(Object.getPrototypeOf(foo)) //Object.prototype

console.log(Object.getPrototypeOf(bar)) //foo

console.log(Object.getPrototypeOf(baz)) //bar

现在想一想,您是否认为 foo bar 和 baz 相互依赖?

现在让我们做同样的constructor样式代码-

function Foo() {}

function Bar() {}

function Baz() {}

Bar.prototype= Object.create(Foo);
Baz.prototype= Object.create(Bar);

var foo= new Foo(),
    bar= new Bar().
    baz= new Baz();

console.log(Object.getPrototypeOf(foo)) //Foo.prototype
console.log(Object.getPrototypeOf(Foo.prototype)) //Object.prototype

console.log(Object.getPrototypeOf(bar)) //Bar.prototype
console.log(Object.getPrototypeOf(Bar.prototype)) //Foo.prototype

console.log(Object.getPrototypeOf(baz)) //Baz.prototype
console.log(Object.getPrototypeOf(Baz.prototype)) //Bar.prototype

后者和前者代码的唯一区别在于后者 foo, bar, baz bbjects 通过任意对象相互链接 他们的constructor 函数(Foo.prototype、Bar.prototype、Baz.prototype)但在前一个(OLOO 风格)中,它们是直接链接的。这两种方式你只是将foo、bar、baz 相互链接,直接在前一种中,在后一种中间接。但是,在这两种情况下,对象都是相互独立的,因为它不像任何类的实例,一旦实例化,就不能从其他类继承。您也可以随时更改对象应该委托的对象。

var anotherObj= {};
Object.setPrototypeOf(foo, anotherObj);

所以它们都是相互独立的。

" 我希望OLOO 能解决每个对象都知道的问题 与其他无关。”

是的,这确实是可能的-

让我们使用Tech 作为实用对象-

 var Tech= {
     tag: "technology",
     setName= function(name) {
              this.name= name;
}
}

创建任意数量的对象链接到Tech-

var html= Object.create(Tech),
     css= Object.create(Tech),
     js= Object.create(Tech);

Some checking (avoiding console.log)- 

    html.isPrototypeOf(css); //false
    html.isPrototypeOf(js); //false

    css.isPrototypeOf(html); //false
    css.isPrototypeOf(js); //false

    js.isPrototypeOf(html); //false
    js.isPrototypwOf(css); //false

    Tech.isPrototypeOf(html); //true
    Tech.isPrototypeOf(css); //true
    Tech.isPrototypeOf(js); //true

你认为html、css、js 对象是相互连接的吗?不,他们不是。现在让我们看看我们如何使用constructor 函数来做到这一点-

function Tech() { }

Tech.prototype.tag= "technology";

Tech.prototype.setName=  function(name) {
              this.name= name;
}

创建任意数量的对象链接到Tech.proptotype-

var html= new Tech(),
     css= new Tech(),
      js= new Tech();

一些检查(避免console.log)-

html.isPrototypeOf(css); //false
html.isPrototypeOf(js); //false

css.isPrototypeOf(html); //false
css.isPrototypeOf(js); //false

js.isPrototypeOf(html); //false
js.isPrototypeOf(css); //false

Tech.prototype.isPrototypeOf(html); //true
Tech.prototype.isPrototypeOf(css); //true
Tech.prototype.isPrototypeOf(js); //true

你觉得这些constructor 风格的对象如何(html,css,js) 对象不同于OLOO 风格的代码?事实上,它们服务于相同的目的。在OLOO 样式中,一个对象委托给Tech(显式设置委托),而在constructor 样式中,一个对象委托给Tech.prototype(隐式设置委托)。最终,您最终将三个相互没有联系的对象链接到一个对象,直接使用OLOO-style,间接使用constructor-style。

“按原样,必须从 ObjA.. Object.create(ObjB) 等创建 ObjB”

不,ObjB 这里不像任何类的实例(在基于经典的语言中) ObjA。 可以说objB 对象在创建时被委托给ObjA 对象 时间”。如果你使用构造函数,你会做同样的“耦合”,虽然间接地通过使用.prototypes。

【讨论】:

    【解决方案5】:

    @Marcus @bholben

    也许我们可以这样做。

        const Point = {
    
            statics(m) { if (this !== Point) { throw Error(m); }},
    
            create (x, y) {
                this.statics();
                var P = Object.create(Point);
                P.init(x, y);
                return P;
            },
    
            init(x=0, y=0) {
                this.x = x;
                this.y = y;
            }
        };
    
    
        const Point3D = {
    
            __proto__: Point,
    
            statics(m) { if (this !== Point3D) { throw Error(m); }},
    
            create (x, y, z) {
                this.statics();
                var P = Object.create(Point3D);
                P.init(x, y, z);
                return P;
            },
    
            init (x=0, y=0, z=0) {
                super.init(x, y);
                this.z = z;
            }
        }; 
    

    当然,创建一个链接到 Point2D 对象原型的 Point3D 对象有点愚蠢,但这不是重点(我想与您的示例保持一致)。无论如何,就投诉而言:

    1. 可以使用 ES6 的 Object.setPrototypeOf 或 我使用的__proto__ = ... 越皱眉。我们现在也可以在常规对象上使用super,如Point3D.init() 所示。另一种方法是做类似

      const Point3D = Object.assign(Object.create(Point), {  
          ...  
      }   
      

      虽然我不是特别喜欢这种语法。


    1. 我们总是可以将p = Object.create(Point) 然后p.init() 包装到构造函数中。例如Point.create(x,y)。使用上面的代码,我们可以通过以下方式创建一个Point3D“实例”。

      var b = Point3D.create(1,2,3);
      console.log(b);                         // { x:1, y:2, z:3 }
      console.log(Point.isPrototypeOf(b));    // true
      console.log(Point3D.isPrototypeOf(b))   // true
      

    1. 我刚刚想出了这个技巧来模拟 OLOO 中的静态方法。我不确定我是否喜欢它。它需要在任何“静态”方法的顶部调用一个特殊属性。例如,我将Point.create() 方法设为静态。

          var p = Point.create(1,2);
          var q = p.create(4,1);          // Error!  
      

    或者,使用 ES6 Symbols,您可以安全地扩展 Javascript 基类。因此,您可以节省一些代码并在 Object.prototype 上定义特殊属性。例如,

        const extendedJS = {};  
    
        ( function(extension) {
    
            const statics = Symbol('static');
    
            Object.defineProperty(Object.prototype, statics, {
                writable: true,
                enumerable: false,
                configurable: true,
                value(obj, message) {
                    if (this !== obj)
                        throw Error(message);
                }
            });
    
            Object.assign(extension, {statics});
    
        })(extendedJS);
    
    
        const Point = {
            create (x, y) {
                this[extendedJS.statics](Point);
                ...
    

    【讨论】:

      【解决方案6】:

      @james emanon - 所以,你指的是多重继承(在“你不知道 JS:this & Object Prototypes”一书的第 75 页上讨论过)。例如,我们可以在下划线的“扩展”功能中找到这种机制。您在示例中陈述的对象名称有点混合了苹果、橙子和糖果,但我理解背后的意义。根据我的经验,这将是 OOLO 版本:

      var ObjA = {
        setA: function(a) {
          this.a = a;
        },
        outputA: function() {
          console.log("Invoking outputA - A: ", this.a);
        }
      };
      
      // 'ObjB' links/delegates to 'ObjA'
      var ObjB = Object.create( ObjA );
      
      ObjB.setB = function(b) {
         this.b = b;
      }
      
      ObjB.setA_B = function(a, b) {
          this.setA( a ); // This is obvious. 'setA' is not found in 'ObjB' so by prototype chain it's found in 'ObjA'
          this.setB( b );
          console.log("Invoking setA_B - A: ", this.a, " B: ", this.b);
      };
      
      // 'ObjC' links/delegates to 'ObjB'
      var ObjC = Object.create( ObjB );
      
      ObjC.setC = function(c) {
          this.c = c;  
      };
      
      ObjC.setA_C = function(a, c) {
          this.setA( a ); // Invoking 'setA' that is clearly not in ObjC shows that prototype chaining goes through ObjB all the way to the ObjA
          this.setC( c );
          console.log("Invoking setA_C - A: ", this.a, " C: ", this.c);
      };
      
      ObjC.setA_B_C = function(a, b, c){
          this.setA( a ); // Invoking 'setA' that is clearly not in ObjC nor ObjB shows that prototype chaining got all the way to the ObjA
          this.setB( b );
          this.setC( c );
          console.log("Invoking setA_B_C - A: ", this.a, " B: ", this.b, " C: ", this.c);
      };
      
      ObjA.setA("A1");
      ObjA.outputA(); // Invoking outputA - A:  A1
      
      ObjB.setA_B("A2", "B1"); // Invoking setA_B - A:  A2  B:  B1
      
      ObjC.setA_C("A3", "C1"); // Invoking setA_C - A:  A3  C:  C1
      ObjC.setA_B_C("A4", "B2", "C1"); // Invoking setA_B_C - A:  A4  B:  B2  C:  C1
      

      这是一个简单的例子,但重点是我们只是将对象以相当扁平的结构/形式链接在一起,并且仍然有可能使用来自多个对象的方法和属性。我们实现了与类/“复制属性”方法相同的事情。 Kyle 总结(第 114 页,“this & Object Prototypes”):

      换句话说,实际的机制,重要的本质 对于我们可以在 JavaScript 中利用的功能,都是关于对象的 链接到其他对象。

      我知道对您来说更自然的方法是在一个地方/函数调用中声明所有“父”(小心:))对象,而不是对整个链进行建模。

      它需要的是根据这一点转变我们的应用程序中的思维和建模问题。我也习惯了。希望它有所帮助,凯尔本人的最终判决会很棒。 :)

      【讨论】:

      • 是的——谢谢——但我希望摆脱这种方法,因为你拥有它的方式,以及我想这样做的方式使得每个 obj 都依赖于另一个。我是希望 OLOO 能解决每个对象对另一个对象一无所知的问题。照原样,必须从 ObjA.. Object.create(ObjB) 等创建 objB。这太耦合了。有什么想法吗?
      【解决方案7】:

      @Marcus,就像你一样,我一直热衷于 OLOO,也不喜欢你第一点中描述的不对称。我一直在玩抽象来恢复对称性。您可以创建一个link() 函数来代替Object.create()。使用时,您的代码可能看起来像这样......

      var Point = {
          init  : function(x,y) {
              this.x = x;
              this.y = y;
          }
      };
      
      
      var Point3D = link(Point, {
          init: function(x,y,z) {
              Point.init.call(this, x, y);
              this.z = z;
          }
      });
      

      请记住,Object.create() 有第二个参数可以传入。这里是利用第二个参数的链接函数。它还允许进行一些自定义配置...

      function link(delegate, props, propsConfig) {
        props = props || {};
        propsConfig = propsConfig || {};
      
        var obj = {};
        Object.keys(props).forEach(function (key) {
          obj[key] = {
            value: props[key],
            enumerable: propsConfig.isEnumerable || true,
            writable: propsConfig.isWritable || true,
            configurable: propsConfig.isConfigurable || true
          };
        });
      
        return Object.create(delegate, obj);
      }
      

      当然,我认为@Kyle 不会支持在 Point3D 对象中隐藏init() 函数。 ;-)

      【讨论】:

      • 回过头来看,我现在认为通过结合Object.assign()和Object.create(),我们可以大大简化上面的link()函数。取而代之的是,我们可以使用:function create(delegate, props) { return Object.assign(Object.create(delegate), props); }。或者更好的是,我们可以使用 Underscore 或 Lodash 使其非常简洁:_.create(delegate, props).
      【解决方案8】:

      有没有办法让 OLOO 超过“两个”对象.. 我包含的所有示例都包含基于示例的示例(请参阅 OP 的示例)。假设我们有以下对象,我们如何创建具有“其他”三个属性的“第四个”对象?唉...

      var Button = {
           init: function(name, cost) {
             this.buttonName = name;
             this.buttonCost = cost;
           }
      }
      
      var Shoe = {
           speed: 100
      }
      
      var Bike = {
           range: '4 miles'
      }
      

      这些对象是任意的,可以包含各种行为。但要点是,我们有“n”个对象,我们的新对象需要这三个对象中的一些东西。

      代替给定的例子ala:

      var newObj = Object.create(oneSingularObject);
          newObj.whatever..
      

      但是,我们的 newObject = (Button, Bike, Shoe)......

      在 OLOO 中实现这一点的模式是什么?

      【讨论】:

      • 这听起来像是“优先组合而不是继承”——一个很棒的策略。在 ES6 中,您可以使用 Object.assign() - developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/…。如果用 ES5 编写,可以使用 Underscore 的 _.extend() 或 Lodash 的 _.assign()。这是一个很好的视频来解释...youtu.be/wfMtDGfHWpA。如果您有任何冲突的属性,最后一个获胜 - 所以顺序很重要。
      猜你喜欢
      • 2011-07-25
      • 2016-03-25
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-12-02
      • 1970-01-01
      相关资源
      最近更新 更多