凯尔·辛普森(Kyle Simpson)的OLOO模式与原型设计模式


109

凯尔·辛普森(Kyle Simpson)的“ OLOO(链接到其他对象的对象)模式”是否与原型设计模式有所不同?除了用专门表示“链接”(原型的行为)的东西创造出来,并澄清这里没有“复制”(类的行为)之外,他的模式究竟引入了什么?

是Kyle在他的书“您不知道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."

2
您至少可以链接到所要询问的模式的描述吗?更好的是在您的问题中显示它的代码示例。
jfriend00 2015年

4
Getify有时在Stackoverflow上。我已经在推特上问了这个问题:)
尖尖的2015年

Answers:


155

他的模式到底引入了什么?

OLOO照原样包含了原型链,而无需依靠其他(IMO令人困惑的)语义来获得链接。

因此,这两个摘要具有完全相同的结果,但到达结果却有所不同。

构造器形式:

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

在两个片段中,一个x对象都链接[[Prototype]]到一个对象(Bar.prototypeBarObj),而对象又链接到第三个对象(Foo.prototypeFooObj)。

片段之间的关系和委托相同。片段之间的内存使用情况相同。片段之间创建多个“孩子”(也就是许多对象,例如x1通过x1000等)的能力是相同的。片段之间的委派(x.yx.z)的性能相同。使用OLOO,对象创建性能较慢,但是通过健全性检查可以发现,性能下降的确不是问题。

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

OLOO只是裁掉中间人。

这是vs OLOO 的另一个比较class


2
我发现您的答案和书中描述的OLOO的想法真的很有趣,我希望收到您对以下问题的反馈:stackoverflow.com/questions/40395762/… 特别是如果您发现此实现正确以及如何解决与访问相关的问题,私人会员。感谢您提前抽出宝贵的时间,并祝贺您获得了最新的书籍。
GibboK '16


3
@Pier的性能实际上并不是什么大问题。修复了断开的博客文章链接,该链接有关健全性检查对象创建的性能,该链接说明了如何正确考虑这一点。
凯尔·辛普森

2
jQuery比DOM API慢,对吗?但是,这是今年,伙计-我宁愿写得优雅而简单,而不用担心优化。如果以后需要进行微优化,那么我会担心的时候到了。
艾里克·伯克兰

6
我想补充一下,仅仅一年多之后,Object.create()在chrome中进行了优化,而jsperf展示了它-它是目前最快的选择之一。这正好说明了为什么您不应该关注这种微优化,而只写算法上合理的代码。
凯尔·贝克

25

我读了凯尔的书,发现它确实很有帮助,特别是有关如何this绑定的细节。

优点:

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

1.简单性

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

2.更简洁的语法

这是有争议的,但是我觉得OLOO语法(在许多情况下)比“标准” javascript方法更简洁,更简洁,尤其是在涉及多态(super-style调用)时。

缺点:

我认为设计中有一个可疑的地方(实际上有助于上面的第二点),这与阴影有关:

在行为委派中,我们尽可能避免在[[Prototype]]链的不同层次上命名相同的事物。

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

潜在的问题是:如果其他人出现并决定制作一个specialResource对象,而没有完全了解整个原型链,那么他们可能会合理地*决定将最后一次保存的时间戳记保存在名为的属性下save,这将掩盖基本save()功能该resource对象在原型链中有两个链接:

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会更好,但这只是一个例子。)


23
我同意潜在的名称冲突是一个缺点……但是实际上这是[[Prototype]]系统本身的缺点,而不是OLOO 的缺点。
凯尔·辛普森

也许这也应该在书中提到?
租赁

我不确定这是否真的解决了@Ed Hinchliffe所描述的问题,因为它只是将save()移至其自己的名称空间,但确实可以工作codepen.io/tforward/pen/govEPr?editors=1010
Tristan Forward

我认为@ ed-hinchliffe的意思是b.timeStampedSave();代替a.timeStampedSave();代码片段的最后一行。
amangpt777 '18年

1
@ tristan-forward谢谢您将Rick和Morty纳入其中!
埃里克·比斯哈德

13

“您不知道JS:这个和对象原型”中的讨论以及OLOO的演示是发人深省的,我在本书中学到了很多东西。OLOO模式的优点在其他答案中有很好的描述。但是,我对它有以下一些宠物投诉(或者缺少使我无法有效应用它的某些东西):

1个

当“类”“继承”经典模式中的另一个“类”时,可以将两个函数声明为相似的语法(“函数声明”或“函数声明”):

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);

相反,在Prototype模式中,您使用标准运算符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));}
};

2
您的第一个代码示例中没有文字。您可能会滥用“字面意义”这个术语,从而赋予它另一种含义。只是说...
Ivan Kleshnin 16/12/2

2
关于第二点,作者认为将创建和初始化分开是“更好”的关注点分离,并列举了在这种情况下可能会发光的一些罕见用例(例如对象池)。我发现论点非常微弱。
租赁

2
关于第二点,再次使用OLOO,您可以一次创建对象,然后等待初始化,而使用构造函数,则必须在创建时进行初始化,因此Kyle认为这是一个好处。
塔科

5

“我想这样做会使每个obj相互依赖”

正如凯尔(Kyle)解释的那样,当两个对象[[Prototype]]链接在一起时,它们并不是真正相互依赖的。相反,它们是个体对象。您正在通过链接将一个对象链接到另一个对象,[[Prototype]]可以随时更改链接。如果您将[[Prototype]]通过OLOO样式创建的两个链接对象视为相互依赖,则对于通过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 barbaz为依赖于每个-其他?

现在,让我们使用相同的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

唯一的区别的B / W后者和前者的代码是在后者 foobarbazbbjects通过它们的任意对象链接到每个-其他constructor功能(Foo.prototypeBar.prototypeBaz.prototype),但在前者(OLOO它们被直接连接样式)。这两种方法你只是链接foobarbaz相互直接在前一个和间接后者。但是,在这两种情况下,对象都是彼此独立的,因为它实际上不像任何实例化的实例,一旦实例化就无法从其他某个类继承。您始终可以更改对象也应该委派哪个对象。

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

你认为htmlcssjs对象相互连接,其他的?不,不是。现在,让我们看看如何使用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风格对象(htmlcssjs)对象从不同OLOO风格的代码?实际上,它们具有相同的目的。在OLOO一种风格中,一个对象委托给Tech(显式设置),而在constructor一种风格中,一个对象委托给Tech.prototype(隐式设置)。最终,您最终直接使用OLOO-style 将三个对象(彼此之间没有链接)链接到一个对象,而间接使用constructor-style。

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

不,ObjB这里不像任何类的实例(基于古典语言) ObjA它被前人的精力说喜欢objB对象是由委托给ObjA它的创建时间”的对象。如果你使用的构造方法,你会做相同的‘耦合’,虽然间接通过利用.prototype秒。


3

@马库斯@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 符号,您可以安全地扩展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);
            ...


2

@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页,“此和对象原型”):

换句话说,对于我们可以在JavaScript中利用的功能而言,真正的机制(对我们而言至关重要的要素的实质)全都在于将对象链接到其他对象

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

所需要的是根据此思路在我们的应用程序中思考和建模问题的转变。我也已经习惯了。希望这会有所帮助,而Kyle本人的最终裁决将是很棒的。:)


是的-谢谢-但是我希望摆脱这种方法,因为您拥有它的方式以及我认为这样做的方式使每个obj相互依赖。我希望OLOO可以解决以下问题:每个对象对对方一无所知。照原样,必须从过于耦合的ObjA..Object.create(ObjB)等创建objB。有任何想法吗?
james emanon 2015年

-1

@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不会赞同init()在Point3D对象中隐藏函数。;-)


现在回想起来,我认为通过Object.assign()与结合使用Object.create(),我们可以大大简化link()上面的功能。可以使用它代替它function create(delegate, props) { return Object.assign(Object.create(delegate), props); }。或者更好的是,我们可以使用Underscore或Lodash使其非常简洁: _.create(delegate, props)
bholben 2015年

-1

有没有一种方法可以对多个“两个”对象进行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中执行此操作?


1
这听起来像是“偏爱组成而不是继承”-一个很好的策略。在ES6,你可以使用Object.assign()- developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/...。如果使用ES5编写,则可以使用_.extend()Underscore或Lodash的_.assign()。这是一个出色的视频,可以解释... youtu.be/wfMtDGfHWpA。如果您有任何冲突的属性,那么最后一个将获胜-因此顺序很重要。
bholben 2015年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.