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

IT技术 javascript design-patterns
2021-02-25 10:48:23

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."
6个回答

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

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

片段之间的关系和委托是相同的。代码段之间的内存使用是相同的。创建许多“子项”(又名,许多对象,如x1throughx1000等)的能力在片段之间是相同的。委托 (x.yx.z)的性能在片段之间是相同的。创建对象的性能与0100比较慢,但理智检查表明,较慢的性能真的不是一个问题。

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

OLOO 只是切断了中间人。

与 OLOO另一个比较class

我发现你的回答和你书中描述的 OLOO 的想法真的很有趣,我想得到你对这个问题的反馈:stackoverflow.com/questions/40395762/... 特别是如果你发现这个实现是正确的以及如何解决与访问相关的问题私人会员。感谢您提前抽出时间并祝贺您的​​最新书。
2021-04-25 10:48:23
jQuery 比 DOM API 慢,对吧?但是,这是今年,伙计 - 我宁愿写得优雅而简单,也不愿担心优化。如果以后需要微优化,到时候再去考虑。
2021-04-27 10:48:23
@Pier 性能实际上并不是什么大问题。修复了有关健全性检查对象创建性能的断开的博客文章链接,它解释了如何正确考虑这一点。
2021-05-03 10:48:23
2021-05-14 10:48:23
现在我想补充一点,仅仅一年多之后,Object.create() 在 chrome 中进行了大量优化,并且 jsperf 显示了它——它是现在最快的选择之一。这恰恰说明了为什么您不应该关注此类微优化,而应该只编写算法上合理的代码。
2021-05-14 10:48:23

我读了凯尔的书,我发现它真的很有用,尤其是关于如何this装订的细节

优点:

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

1. 简单

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

2. 更简洁的语法

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

缺点:

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

在行为委托中,我们尽可能避免在[[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会好很多,但这只是一个例子。)

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

“你不知道 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);
    

相反,在原型模式中,您使用标准运算符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));}
};
关于第二点,作者认为将创建和初始化分开是一个“更好的”关注点分离,并引用了一些可能会发光的罕见用例(例如对象池)。我觉得这个论点非常无力。
2021-04-21 10:48:23
@IvanKleshnin“对象文字是用大括号括起来的名称-值对的逗号分隔列表”。该对象{init: function(x,y) ..}已分配给VARPoint在第一示例的第二部分是在我的书的对象文本。
2021-05-03 10:48:23
您的第一个代码示例中没有文字。您可能误用了“文字”一词​​,赋予了它另一种含义。就是说...
2021-05-09 10:48:23
再次关于第二点,使用 OLOO,您可以一次创建对象,然后等待初始化,而使用构造函数,您必须在创建时进行初始化,因此 Kyle 认为这是一个好处。
2021-05-09 10:48:23

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

正如凯尔所解释的,当两个对象被[[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 后者和前者代码的唯一区别在于,在后一个 foo, 中barbaz对象通过其constructor函数 ( Foo.prototype, Bar.prototype, Baz.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

你认为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-style Objects ( html, css, js) Objects 与OLOO-style 代码有何不同事实上,它们的目的是一样的。OLOO-style one 对象委托给Tech(委托被显式设置)而在constructor-style one 对象委托给Tech.prototype(委托被隐式设置)。最终,您最终将三个对象(彼此之间没有链接)链接到一个对象,直接使用OLOO-style,间接使用constructor-style。

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

不,ObjB这里不像任何类的实例(在基于古典的语言中) ObjA应该说对象在创建时objB被委托给ObjA对象”。如果您使用构造函数,您会完成相同的“耦合”,尽管是通过使用.prototypes间接完成的。

@马库斯@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. 我刚刚想出了这个 hack 来模拟 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);
            ...