JavaScript中的原型OO

TL; DR:

我们是否需要原型OO中的工厂/构造器? 我们可以做一个范例转换并完全放弃它们吗?

BackStory:

我最近一直在做JavaScript中的原型OO,并发现在JavaScript中执行的面向对象的99%迫使经典的OO模式。

我认为原型OO是两件事。 方法的静态原型(和静态数据)和数据绑定。 我们不需要工厂或build设者。

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

这意味着我们可以将所有东西都build模为一个静态的蓝图/原型和一个数据绑定抽象,这个抽象最好直接插入到文档样式的数据库中。 即从数据库中取出对象,并通过数据克隆原型来创build对象。 这将意味着没有构造逻辑,没有工厂,没有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, {...})来创build。

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

当一个实体第一次被创build时,它被创build为一个空的{}

实际的问题:

现在我的实际问题是

  • JS原型OO的开源示例?
  • 这是一个好主意吗?
  • 它是否与原型OOP背后的想法和概念一致?
  • 不会使用任何构造函数/工厂函数咬我的屁股的地方? 我们真的可以逃避不使用构造函数。 使用上述方法有什么限制,我们需要工厂去克服它们。

根据你的评论,这个问题主要是“构造知识是必要的吗? 我感觉到了。

一个玩具的例子就是存储部分数据。 对于内存中给定的数据集,当持久存储时,我只能select存储某些元素(为了效率或数据一致性的目的,例如一旦持久存在就固有地无用)。 让我们开始一个会话,在其中存储用户名和他们点击帮助button的次数(因为没有更好的例子)。 当我坚持这个在我的例子中,我没有用的点击数,因为我现在保存在内存中,下一次我加载数据(下次用户login或连接或任何)我将初始化从头开始(大概为0)。 这个特殊的用例是构造逻辑的一个很好的select。

唉,但是你总是可以把它embedded到静态原型中: Object.create({name:'Bob', clicks:0}); 当然,在这种情况下。 但是,如果价值一开始并不总是0,而是需要计算的东西。 Uummmm,比方说,用户年龄(秒)(假设我们存储了名字和DOB)。 同样,一个没有什么用处的项目仍然存在,因为无论如何,它将需要在检索时重新计算。 那么如何将用户的年龄存储在静态原型中呢?

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

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

UPDATE

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

例如,我将要求每个游戏实例不只是一张静态/硬编码的纸牌,而是一个随机洗牌的纸牌。 如果是静态的,那么用户每次都会玩同样的游戏,这显然不好。

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

你如何做一个静态的Object.create()

我不认为构造函数/工厂逻辑是必要的,只要你改变你对面向对象编程的看法。 在我最近对这个主题的探讨中,我发现Prototypicalinheritance更适合于定义一组使用特定数据的函数。 对于那些经典inheritance培训的人来说,这不是一个外国概念,但是这些“父”对象并没有定义要操作的数据。

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

因此,在上面的例子中,我们创build了与动物一起的function,然后创build一个具有该function的对象,以及完成这些动作所需的数据。 对于任何习惯于经典遗传的人来说,这会感到不舒服和奇怪。 它没有公共/私人/被保护的成员可视性等级的模糊性,我会第一个承认它让我感到紧张。

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

现在,inheritance。

我一直理解JavaScript中的JavaScript是困难的。 浏览原型链的来龙去脉并不完全清楚。 直到使用Object.create ,它将所有基于函数的新关键字redirect移出等式。

比方说,我们想扩大上面的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又以原型为原型。 很简单。 没有误导,没有“原型等同于函数原型的新对象”。 只是简单的原型inheritance。 这很简单,直接。 多态性也很容易。

 human.speak = function() { console.log(this.favoriteWord + ', dudes'); } 

由于原型链的运作方式, myHuman.speakanimal被发现之前就会在human身上find,因此我们的人是一个冲浪者,而不是一个无聊的老动物。

所以,最后( TLDR ):

伪古典的构造函数被join到JavaScript中,使那些在古典OOP中训练的程序员更加舒适。 这绝不是必要的,但它意味着放弃经典概念,如成员可见性和(重复式)构造函数。

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

inheritance的简单性实在令人难以置信。 我已经阅读了很多关于JavaScript的inheritance,并且写了很多代码尝试理解它。 但是,它真的归结为从其他对象inheritance的对象 。 就像这样简单,所有new关键字都会混淆起来。

这种灵活性很难被充分利用,而且我相信我还没有做到,但它在那里,导航很有趣。 我认为大部分没有被用于大型项目的原因是,它不是很好理解,恕我直言,我们被locking在我们都学到的经典的inheritance模式被教授C ++,Java等

编辑

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

经过进一步的思考,在这段时间里,我已经好几次翻到了围栏的两边,所以我得出的结论是,工厂也是不必要的。 如果animal (以上)被赋予了另一个函数的initialize ,创build并初始化一个从animalinheritance的新对象将是微不足道的。

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

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

@Raynos – 你完全是书呆子狙击我的这一个。 我应该准备好五天,完全没有什么生产力。

静态可克隆“types”的示例:

 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 } } 

现在,我们可以在其他地方克隆这种types,是吗? 当然,我们需要使用var myType = Object.Create(MyType) ,但是我们完成了,是吗? 现在我们只能使用myType.size ,这就是事物的大小。 或者我们可以读取颜色,或者改变颜色等。我们还没有创build构造函数或者任何东西,对吧?

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

 // 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 } } 

因为我们已经创造了所有我们想要的东西,而且我们已经定义了一切。 这是一个构造函数。 所以即使我们只克隆/使用静态的东西(这就是我看到的上面的代码片段),我们仍然有一个构造函数。 只是一个静态的构造函数。 通过定义一个types,我们定义了一个构造函数。 替代scheme是这种对象构造的模型:

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

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

对你的问题的简短回答“我们是否需要工厂/build设者在典型的面向对象? 没有。 工厂/构造函数仅用于一个目的:将新创build的对象(实例)初始化为特定的状态。

这就是说,经常使用,因为有些对象需要某种初始化代码。

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

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

现在下面的代码将创build基于“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) 

那为什么呢? 答案很简单:因为基于原型的inheritance。 当我们创build对象时,没有任何代码在实际实例上设置属性idcreatedTime ,正如在构造函数/工厂中一样。 因此,当访问属性时,它从原型链中拉出,这对于所有实体来说都是单一值。

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

现在,在实现将所有原型作为静态方法和属性集合处理(以及如此)的过程中,通过将属性的值分配给数据源中的数据来初始化对象。 它可能不是使用new或某种types的工厂,而是初始化代码。

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