43

TL;博士:

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

幕后故事:

最近我一直在玩弄用 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的属性才会存储在数据库中。

第一次创建实体时,它是用空的{}

实际问题:

现在我的实际问题是

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

4 回答 4

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.create,这将所有基于函数的新关键字重定向排除在外。

假设我们想扩展上述animal对象,并​​制作一个人。

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.speakhuman在它被发现之前被发现animal,因此我们的人类是一个冲浪者,而不仅仅是一个无聊的老动物。

所以,总而言之(TLDR):

伪经典的构造函数功能有点附加到 JavaScript 上,以使那些受过经典 OOP 培训的程序员更加舒适。无论如何,这不是必需的,但它意味着放弃经典概念,例如成员可见性和(同义反复的)构造函数。

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

继承的简单性确实令人难以置信。我读过很多关于 JavaScript 继承的文章,并写了很多行代码试图理解它。但它实际上归结为从其他对象继承的对象。就这么简单,new关键字所做的就是把它搞混。

这种灵活性很难充分利用,我敢肯定我还没有这样做,但它就在那里,而且导航很有趣。我认为它没有被用于大型项目的大部分原因是它根本没有被理解,而且,恕我直言,我们被锁定在我们学习的经典继承模式中学过C++、Java等。

编辑

我想我已经为构造函数做了一个很好的案例。但我反对工厂的论点是模糊的。

经过进一步的思考,我在栅栏的两边翻了几次,得出的结论是,工厂也没有必要。如果animal(上面)被赋予了另一个函数initialize,那么创建和初始化一个继承自的新对象将是微不足道的animal

var myDog = Object.create(animal);
myDog.initialize(4, 'Meow');

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

@Raynos - 你完全是书呆子在这个问题上狙击了我。我应该为 5 天毫无成效的工作做好准备。

于 2011-06-29T21:17:13.633 回答
11

根据您的评论,问题主要是“是否需要构造函数知识?” 我觉得是。

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

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

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

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

更新

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

例如,我将在每个游戏实例上不仅需要静态/硬编码的纸牌,还需要随机洗牌的纸牌。如果它是静态的,那么用户每次都会玩同样的游戏,这显然是不好的。

如果玩家用完,我可能还需要启动计时器来完成游戏。同样,这不是静态的,因为我的游戏有一些要求:秒数与连接玩家迄今为止赢得的游戏数量成反比(同样,没有保存的信息,这个连接有多少) ,与洗牌的难度成正比(有一种算法可以根据洗牌的结果来判断游戏的难易程度)。

您如何使用 static 做到这一点Object.create()

于 2011-06-29T20:20:04.670 回答
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) 并且当你这样做时,你将使用静态对象来创建目标对象。然后它就和前面的例子一样了。

于 2011-06-29T21:59:02.080 回答
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 原型 OOPnew中 - 不需要 - 不需要工厂 - 通常需要初始化代码,这通常通过new、 工厂或其他一些您不想承认正在初始化对象的实现来完成

于 2011-06-30T16:54:55.950 回答