Kyle Simpson 的 OLOO 模式与原型设计模式

Kyle Simpson's OLOO Pattern vs Prototype Design Pattern

Kyle Simpson 的 "OLOO (Objects Linking to Other Objects) Pattern" 与 Prototype 设计模式有何不同?除了通过具体指示 "linking"(原型的行为)并澄清这里没有 "copying" 发生(类 的行为)之外,他的模式到底介绍了什么?

这是他的书 an example of Kyle's pattern "You Don't Know 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."

what exactly does his pattern introduce?

OLOO 按原样包含原型链,无需叠加其他(IMO 令人困惑的)语义来获得 linkage。

因此,这两个片段具有完全相同的结果,但到达那里的方式不同。

构造函数形式:

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]]-linked 到一个对象(Bar.prototypeBarObj),而后者又是linked 到第三个对象(Foo.prototypeFooObj)。

片段之间的关系和委派是相同的。片段之间的内存使用情况是相同的。创建许多 "children"(又名,许多对象,如 x1x1000 等)的能力在片段之间是相同的。委托(x.yx.z)的性能在片段之间是相同的。 OLOO 的对象创建性能 较慢,但 sanity checking that 表明性能较慢确实不是问题。

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

OLOO就是去掉了中间人

这是 class 与 OLOO 的 another comparison

我读了 Kyle 的书,我发现它非常有用,尤其是关于如何绑定 this 的细节。

优点:

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

1。简单

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

2。更简洁的语法

这是有争议的,但我觉得 OLOO 语法(在许多情况下)比 'standard' javascript 方法更简洁、更简洁,尤其是在涉及多态时(super 式调用)。

缺点:

我认为设计中有一点值得怀疑(实际上对上述第 2 点有贡献),那就是与阴影有关:

In behaviour delegation, we avoid if at all possible naming things the same at different levels of the [[Prototype]] chain.

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

潜在的问题是:如果其他人出现并决定制作一个 specialResource 对象,而不完全了解整个原型链,他们可能合理地*决定为最后一次保存保存时间戳一个名为 save 的 属性,它隐藏了 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会好很多,只是举个例子)

"You Don't Know JS: this & Object Prototypes" 中的讨论和 OLOO 的介绍发人深省,我通过这本书学到了很多东西。 OLOO 模式的优点在其他答案中有详细描述;但是,我对它有以下宠物抱怨(或者缺少一些阻止我有效应用它的东西):

1

当一个"class""inherits"另一个"class"在经典模式中,这两个函数可以声明类似的语法("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

在经典模式中,我可以通过将它们直接分配给 "class" 函数(与其 .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 模式中,对象实例上的任何 "static" 函数也可用(通过 [[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));}
};

@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() 有一个可以传入的第二个参数。这里是利用第二个参数的 link 函数。它还允许一些自定义配置...

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() 函数。 ;-)

有没有办法让 OLOO 超过 "two" 个对象.. 我包含的所有示例都是基于示例的(请参阅 OP 的示例)。假设我们有以下对象,我们如何创建一个具有 'other' 三个属性的 "fourth" 对象?阿拉...

var Button = {
     init: function(name, cost) {
       this.buttonName = name;
       this.buttonCost = cost;
     }
}

var Shoe = {
     speed: 100
}

var Bike = {
     range: '4 miles'
}

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

而不是给定的例子:

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

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

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

@james emanon - 所以,你指的是多重继承(在书 "You Don't Know JS: this & Object Prototypes" 的第 75 页上讨论)。例如,我们可以在下划线的 "extend" 函数中找到该机制。您在示例中陈述的对象名称有点混合了苹果、橙子和糖果,但我理解背后的意义。根据我的经验,这将是 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

这是一个简单的示例,但要点是我们只是将对象以相当平坦的方式链接在一起 structure/formation,并且仍然有可能使用来自多个对象的方法和属性。我们实现了与 class/"copying the properties" 方法相同的效果。凯尔总结(第 114 页,"this & Object Prototypes"):

In other words, the actual mechanism, the essence of what’s important to the functionality we can leverage in JavaScript, is all about objects being linked to other objects.

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

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

"I figured to do it makes each obj dependent on the other"

正如 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 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是后者的代码 foobarbaz bbjects 通过任意对象相互链接 它们的 constructor 功能(Foo.prototypeBar.prototypeBaz.prototype),但在前一个(OLOO 样式)中,它们是直接链接的。这两种方式都只是将 foobarbaz 彼此链接起来,前者直接链接,后者间接链接。但是,在这两种情况下,对象都是相互独立的,因为它不像任何 class 的实例,一旦实例化,就不能从其他 class 继承。您也可以随时更改一个对象应该委托给哪个对象。

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

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

" I was hoping OLOO would solve the issue in which each object knows nothing about the other."

是的,这确实是可能的-

让我们使用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 风格的对象 (html , css, js) 对象不同于 OLOO 风格的代码?事实上,它们服务于相同的目的。在 OLOO 风格中,一个对象委托给 Tech(委托是明确设置的),而在 constructor 风格中,一个对象委托给 Tech.prototype(委托是隐式设置的)。最终,您最终将三个彼此没有链接的对象链接到一个对象,直接使用 OLOO-style,间接使用 constructor-style.

"As is, ObjB has to be created from ObjA.. Object.create(ObjB) etc"

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

@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 "instance"。

    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. 我刚刚想到了这个 hack 来模拟 OLOO 中的静态方法。我不确定我喜不喜欢。它需要在任何 "static" 方法的顶部调用一个特殊的 属性。例如,我已将 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);
            ...