JavaScript中的原型OO

时间:2011-06-29 19:38:22

标签: javascript oop node.js prototypal-inheritance ecmascript-5

TL; DR:

我们是否需要原型OO中的工厂/构造函数?我们可以进行范式切换并将其完全丢弃吗?

BackStory:

我最近一直在玩JavaScript中的原型OO并发现在JavaScript中完成的99%的OO正在强制使用经典的OO模式。

我对原型OO的看法是它涉及两件事。方法(和静态数据)的静态原型和数据绑定。我们不需要工厂或建造者。

在JavaScript中,这些是包含函数和Object.create的对象文字。

这意味着我们可以将所有内容建模为静态蓝图/原型和数据绑定抽象,最好直接挂钩到文档样式的数据库中。即将对象从数据库中取出并通过使用数据克隆原型来创建。这意味着没有构造函数逻辑,没有工厂,没有new

示例代码:

一个伪示例是:

var Entity = Object.create(EventEmitter, {
    addComponent: {
        value: function _addComponent(component) {
            if (this[component.type] !== undefined) {
                this.removeComponent(this[component.type]);
            }

            _.each(_.functions(component), (function _bind(f) {
                component[f] = component[f].bind(this);
            }).bind(this));

            component.bindEvents();

            Object.defineProperty(this, component.type, {
                value: component,
                configurable: true
            });

            this.emit("component:add", this, component);
        }
    },
    removeComponent: {
        value: function _removeComponent(component) {
            component = component.type || component;

            delete this[component];

            this.emit("component:remove", this, component);
        }
    }
}

var entity = Object.create(Entity, toProperties(jsonStore.get(id)))

次要解释:

特定代码很冗长,因为ES5很冗长。上面的Entity是蓝图/原型。将使用Object.create(Entity, {...})创建包含数据的任何实际对象。

实际数据(在本例中为组件)直接从JSON存储加载并直接注入Object.create调用。当然,类似的模式应用于创建组件,只有传递Object.hasOwnProperty的属性存储在数据库中。

首次创建实体时,使用空{}

创建实体

实际问题:

现在我的实际问题是

  • Open source examples of JS prototypical OO?
  • 这是个好主意吗?
  • 是否符合原型OOP背后的想法和概念?
  • 不会使用任何构造函数/工厂函数在某个地方咬我?我们真的可以逃避不使用构造函数。使用上述方法是否有任何限制,我们需要工厂来克服它们。

4 个答案:

答案 0 :(得分:26)

我认为构造函数/工厂逻辑根本不是必需的,只要你改变你对面向对象编程的看法。在我最近对该主题的探索中,我发现原型继承更多地用于定义一组使用特定数据的函数。对于经典继承训练的人来说,这不是一个外国概念,但遗憾的是这些“父”对象没有定义要操作的数据。

var animal = {
    walk: function()
    {
        var i = 0,
            s = '';
        for (; i < this.legs; i++)
        {
            s += 'step ';
        }

        console.log(s);
    },
    speak: function()
    {
        console.log(this.favoriteWord);
    }
}

var myLion = Object.create(animal);
myLion.legs = 4;
myLion.favoriteWord = 'woof';

因此,在上面的示例中,我们创建了与动物一起的功能,然后创建具有该功能的对象以及完成操作所需的数据。对于任何习惯于经典继承任何时间长度的人来说,这都让人觉得不舒服和奇怪。它没有公众/私人/受保护的成员知名度等级的温暖模糊性,我将是第一个承认它让我感到紧张的人。

另外,当我看到myLion对象的上述初始化时,我的第一直觉就是为动物创建一个工厂,所以我可以通过简单的功能创建狮子,老虎和熊(我的)呼叫。而且,我认为,对于大多数程序员来说,这是一种自然的思维方式 - 上述代码的冗长是丑陋的,似乎缺乏优雅。我还没有决定是否仅仅是因为经典训练,或者这是否是上述方法的实际错误。

现在,继承。

我一直都很清楚JavaScript中的内容。导航原型链的细节并不十分清楚。直到您将Object.createanimal一起使用,它将所有基于函数的新关键字重定向排除在等式之外。

假设我们想扩展上面的var human = Object.create(animal) human.think = function() { console.log('Hmmmm...'); } var myHuman = Object.create(human); myHuman.legs = 2; myHuman.favoriteWord = 'Hello'; 对象,并创建一个人。

human

这会创建一个以animal为原型的对象,而原型又以human.speak = function() { console.log(this.favoriteWord + ', dudes'); } 为原型。很容易。没有错误的方向,没有“原型等于函数原型的新对象”。只是简单的原型继承。这很简单,也很简单。多态性也很容易。

myHuman.speak

由于原型链的工作方式,human会在animal之前找到var child = Object.create(myHuman),因此我们的人类是冲浪者,而不仅仅是一个无聊的老动物

所以,总之( TLDR ):

伪经典构造函数功能有点强调JavaScript,使那些受过古典OOP训练的程序员更加舒适。无论如何,是必要的,但它意味着放弃经典概念,例如成员可见性和(重复)构造函数。

您获得的回报是灵活性和简单性。您可以动态创建“类” - 每个对象本身就是其他对象的模板。设置子对象的值不会影响这些对象的原型(即如果我使用child.walk = 'not yet',然后设置animal.walknew将不受影响 - 真的,测试它。

继承的简单性是老实说令人难以置信的。我已经阅读了很多关于JavaScript的继承,并编写了许多代码来试图理解它。但它实际上归结为对象继承自其他对象。就这么简单,所有animal关键字都会让人感到困惑。

这种灵活性很难在最大程度上使用,我确信我还没有这样做,但它就在那里,导航很有意思。我认为它没有用于大型项目的大部分原因是它根本不被理解,而且,恕我直言,我们被锁定在我们所学到的经典继承模式中曾教过C ++,Java等。

修改

我认为我对构造函数提出了相当不错的理由。但我反对工厂的论点很模糊。

在进一步思考之后,我多次翻转到篱笆的两侧,我得出的结论是,工厂也是不必要的。如果initialize(上面)给了另一个函数animal,那么创建和初始化一个继承自var myDog = Object.create(animal); myDog.initialize(4, 'Meow'); 的新对象将是微不足道的。

{{1}}

新对象,已初始化并可供使用。

@Raynos - 你完全是书呆子在这一个上辱骂我。我应该准备好连续5天不做任何工作。

答案 1 :(得分:11)

根据你的评论,问题主要是“构建者知识是否必要?”我觉得是。

玩具示例将存储部分数据。在存储器中的给定数据集上,当持久存在时,我可能只选择存储某些元素(为了效率或数据一致性目的,例如,一旦持久存在,这些值本身就是无用的)。让我们来一个会话,我存储用户名和他们点击帮助按钮的次数(缺少一个更好的例子)。当我在我的例子中坚持这一点时,我确实没有使用点击次数,因为我现在将它保存在内存中,下次我加载数据(下次用户登录或连接或其他)我将初始化从头开始的价值(大概是0)。这个特殊用例是构造函数逻辑的一个很好的候选者。

Aahh,但你总是可以将它嵌入到静态原型中:Object.create({name:'Bob', clicks:0});当然,在这种情况下。但是,如果该值一开始并不总是0,而是需要计算的东西,那该怎么办呢?例如,Uummmm用户以秒为单位(假设我们存储了名称和DOB)。同样,一个很少使用的项目仍然存在,因为它无论如何都需要在检索时重新计算。那么如何将用户的年龄存储在静态原型中呢?

显而易见的答案是构造函数/初始化逻辑。

还有很多场景,虽然我觉得这个想法与js oop或特别是任何语言都没有多大关系。实体创建逻辑的必要性是我看待计算机系统模拟世界的方式所固有的。有时我们存储的项目将是一个简单的检索并注入到原型shell之类的蓝图中,有时这些值是动态的,需要初始化。

<强>更新

好的,我将尝试更真实的例子,并避免混淆假设我没有数据库,不需要保留任何数据。假设我正在制作单人纸牌服务器。每个新游戏(自然地)都是Game原型的新实例。我很清楚,这里需要初始化逻辑(还有很多):

例如,我将在每个游戏实例上不仅需要静态/硬编码的卡片组,而且需要随机洗牌。如果它是静态的,用户每次都会玩同一个游戏,这显然不是很好。

如果玩家用完,我可能还需要启动计时器才能完成游戏。同样,不是可以是静态的东西,因为我的游戏有一些要求:秒数与连接的玩家到目前为止赢得的游戏数量成反比(再次,没有保存的信息,这个连接有多少) ,并且与洗牌的难度成比例(根据洗牌结果有一种算法可以确定游戏的难度)。

如何使用静态Object.create()

进行此操作

答案 2 :(得分:2)

静态克隆“类型”的示例:

var MyType = {
  size: Sizes.large,
  color: Colors.blue,
  decay: function _decay() { size = Sizes.medium },
  embiggen: function _embiggen() { size = Sizes.xlarge },
  normal: function _normal() { size = Sizes.normal },
  load: function _load( dbObject ) { 
    size = dbObject.size
    color = dbObject.color 
  }
}

现在,我们可以在别处克隆此类型,是吗?当然,我们需要使用var myType = Object.Create(MyType),但是我们已经完成了,是吗?现在我们可以myType.size,这就是事物的大小。或者我们可以读取颜色,或者更改它等等。我们还没有创建构造函数或其他任何东西,对吧?

如果你说那里没有构造函数,你就错了。让我告诉你构造函数的位置:

// The following var definition is the constructor
var MyType = {
  size: Sizes.large,
  color: Colors.blue,
  decay: function _decay() { size = Sizes.medium },
  embiggen: function _embiggen() { size = Sizes.xlarge },
  normal: function _normal() { size = Sizes.normal },
  load: function _load( dbObject ) { 
    size = dbObject.size
    color = dbObject.color 
  }
}

因为我们已经离开并创造了我们想要的所有东西,我们已经定义了一切。这都是构造函数的作用。因此,即使我们只克隆/使用静态内容(我在上面看到上面的代码片段),我们仍然有一个构造函数。只是一个静态构造函数。通过定义类型,我们定义了一个构造函数。另一种选择是这种对象构造模型:

var MyType = {}
MyType.size = Sizes.large

但最终你会想要使用Object.Create(MyType),当你这样做时,你将使用一个静态对象来创建目标对象。然后它变得与前面的例子相同。

答案 3 :(得分:2)

对您的问题的简短回答“我们是否需要原型OO中的工厂/施工人员?”没有。工厂/构造函数仅用于1个目的:将新创建的对象(实例)初始化为特定状态。

话虽如此,它经常被使用,因为某些对象需要某种初始化代码。

让我们使用您提供的基于组件的实体代码。典型的实体只是一个组件集合和很少的属性:

var BaseEntity = Object.create({},
{
    /* Collection of all the Entity's components */
    components:
    {
        value: {}
    }

    /* Unique identifier for the entity instance */
    , id:
    {
        value: new Date().getTime()
        , configurable: false
        , enumerable: true
        , writable: false
    }

    /* Use for debugging */
    , createdTime:
    {
        value: new Date()
        , configurable: false
        , enumerable: true
        , writable: false
    }

    , removeComponent:
    {
        value: function() { /* code left out for brevity */ }
        , enumerable: true
        , writable: false
    }

    , addComponent:
    {
        value: function() { /* code left out for brevity */ }
        , enumerable: true
        , writable: false
    }
});

现在,以下代码将基于'BaseEntity'

创建新实体
function CreateEntity()
{
    var obj = Object.create(BaseEntity);

    //Output the resulting object's information for debugging
    console.log("[" + obj.id + "] " + obj.createdTime + "\n");

    return obj;
}

看起来很直接,直到你去参考属性:

setTimeout(CreateEntity, 1000);
setTimeout(CreateEntity, 2000);
setTimeout(CreateEntity, 3000);

输出:

[1309449384033] Thu Jun 30 2011 11:56:24 GMT-0400 (EDT)
[1309449384033] Thu Jun 30 2011 11:56:24 GMT-0400 (EDT)
[1309449384033] Thu Jun 30 2011 11:56:24 GMT-0400 (EDT)

那为什么呢?答案很简单:因为基于原型的继承。当我们创建对象时,没有任何代码可以在实际实例上设置属性idcreatedTime,这通常在构造函数/工厂中完成。因此,当访问该属性时,它会从原型链中拉出,最终成为所有实体的单个值。

这个参数是应该传递Object.create()第二个参数来设置这个值。我的回答只是:这与调用构造函数或使用工厂基本相同吗?这只是设置对象状态的另一种方式。

现在,通过实现,您将所有原型作为静态方法和属性的集合处理(并且理所当然地),您可以通过将属性的值分配给数据源中的数据来初始化对象。它可能没有使用new或某种类型的工厂,但它是初始化代码。

总结一下: 在JavaScript原型OOP中 - 不需要new - 不需要工厂 - 通常需要初始化代码,通常通过new,工厂或您不想承认的其他实现来初始化对象