JavaScript 中的原型 OO
TL;DR:
在原型 OO 中我们需要工厂/构造函数吗?我们能否转变范式并彻底放弃它们?
背景故事:
我最近一直在尝试在 JavaScript 中进行原型 OO,发现 JavaScript 中 99% 的 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 背后的思想和概念吗?
- 不使用任何构造函数/工厂函数会在某个地方咬我的屁股吗?我们真的可以不使用构造函数吗?使用上述方法是否有任何限制,我们需要工厂来克服它们。
TL;DR:
Do we need factories/constructors in prototypical OO? Can we make a paradigm switch and drop them completely?
The BackStory:
I've been toying with doing prototypical OO in JavaScript lately and find that 99% of OO done in JavaScript is forcing classical OO patterns into it.
My take on prototypical OO is that it involves two things. A static prototype of methods (and static data) and a data binding. We don't need factories or constructors.
In JavaScript these are Object literals containing functions and Object.create
.
This would mean we can model everything as a static blueprint/prototype and a data binding abstraction that's preferably hooked straight into a document-style database. I.e. objects are taken out of the database and created by cloning a prototype with the data. This would mean there is no constructor logic, no factories, no new
.
The Example code:
An pseudo example would be :
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)))
The minor explanation:
The particular code is verbose because ES5 is verbose. Entity
above is a blueprint/prototype. Any actual object with data would be created by using Object.create(Entity, {...})
.
The actual data (in this case the components) is directly loaded from a JSON store and injected directly into the Object.create
call. Of course a similar pattern is applied to creating components and only properties that pass Object.hasOwnProperty
are stored in the database.
When an entity is created for the first time it's created with an empty {}
The actual Questions:
Now my actual questions are
- Open source examples of JS prototypical OO?
- Is this a good idea?
- Is it in-line with the ideas and concepts behind prototypical OOP?
- Will not using any constructors/factory functions bite me in the ass somewhere? Can we really get away with not using constructors. Are there any limitations using the above methodology where we would need factories to overcome them.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(4)
我认为构造函数/工厂逻辑根本没有必要,只要您改变对面向对象编程的看法即可。在我最近对该主题的探索中,我发现原型继承更适合定义一组使用特定数据的函数。对于接受过经典继承训练的人来说,这并不是一个陌生的概念,但问题是这些“父”对象没有定义要操作的数据。
因此,在上面的示例中,我们创建了与动物相关的功能,然后创建一个具有该功能的对象以及完成操作所需的数据。对于任何已经习惯了经典继承的人来说,无论时间长短,这都会让人感到不舒服和奇怪。它没有成员可见性的公共/私人/受保护层次结构的温暖模糊性,我将是第一个承认这让我紧张的人。
另外,当我看到上面的 myLion 对象初始化时,我的第一直觉是为动物创建一个工厂,这样我就可以用一个简单的函数创建狮子、老虎和熊(天哪)称呼。而且,我认为,这对大多数程序员来说是一种自然的思维方式——上面的代码冗长难看,而且似乎缺乏优雅。我还没有确定这是否仅仅是由于经典训练造成的,或者是否是上述方法的实际错误。
现在,谈谈继承。
我一直认为 JavaScript 中的继承是很困难的。原型链的来龙去脉并不完全清楚。直到您将其与
Object.create
一起使用,这将消除所有基于函数的新关键字重定向。假设我们想要扩展上面的
animal
对象,并创建一个人类。这将创建一个以人类为原型的对象,而该对象又以动物为原型。很容易。没有误导,没有“原型等于函数原型的新对象”。只是简单的原型继承。这很简单,也很直接。多态性也很容易。
由于原型链的工作方式,
myHuman.speak
会先在human
中找到,然后再在animal
中找到,因此我们的人类是冲浪者而不仅仅是一个无聊的老动物。所以,总而言之(TLDR):
伪经典构造函数功能有点附加到 JavaScript 上,以使那些接受过经典 OOP 培训的程序员更加舒适。无论如何,这都不是必要的,但它意味着放弃成员可见性和(同义反复的)构造函数等经典概念。
您得到的回报是灵活性和简单性。您可以即时创建“类” - 每个对象本身就是其他对象的模板。在子对象上设置值不会影响这些对象的原型(即,如果我使用
var child = Object.create(myHuman)
,然后设置child.walk = 'not Yet'< /code>,
animal.walk
不会受到影响 - 真的,测试一下)。继承的简单性确实令人难以置信。我读了很多关于 JavaScript 继承的文章,并编写了很多行代码试图理解它。但它实际上可以归结为对象从其他对象继承。就这么简单,而
new
关键字所做的只是把它搞乱了。这种灵活性很难充分利用,而且我确信我还没有做到这一点,但它确实存在,而且导航起来很有趣。我认为它没有被用于大型项目的大部分原因是它根本没有被充分理解,而且,恕我直言,我们被锁定在我们在学习时都学到的经典继承模式中。教过 C++、Java 等。
编辑
我认为我已经提出了一个很好的反对构造函数的案例。但我反对工厂的论点是模糊的。
经过进一步的思考,我多次摇摆不定,得出的结论是工厂也是没有必要的。如果为
animal
(上图)赋予另一个函数initialize
,那么创建并初始化一个继承于animal
的新对象将是微不足道的。新对象已初始化并可供使用。
@Raynos - 你完全是个书呆子在这件事上狙击了我。我应该做好五天毫无成果的准备。
I don't think the constructor/factory logic is necessary at all, as long as you change how you think about Object-Oriented Programming. In my recent exploration of the topic, I've discovered that Prototypical inheritance lends itself more to defining a set of functions that use particular data. This isn't a foreign concept to those trained in classical inheritance, but the hitch is that these "parent" objects don't define the data to be operated on.
So, in the above example, we create the functionality that goes along with an animal, and then create an object that has that functionality, along with the data necessary to complete the actions. This feels uncomfortable and odd to anyone who's used to classical inheritance for any length of time. It has none of the warm fuzziness of the public/private/protected hierarchy of member visibility, and I'll be the first to admit that it makes me nervous.
Also, my first instinct, when I see the above initialization of the
myLion
object is to create a factory for animals, so I can create lions, and tigers, and bears (oh my) with a simple function call. And, I think, that's a natural way of thinking for most programmers - the verbosity of the above code is ugly, and seems to lack elegance. I haven't decided whether that's simply due to classical training, or whether that's an actual fault of the above method.Now, on to inheritance.
I have always understood inhertance in JavaScript to be difficult. Navigating the ins and outs of the prototype chain is not exactly clear. Until you use it with
Object.create
, which takes all the function-based, new-keyword redirection out of the equation.Let's say we wanted to extend the above
animal
object, and make a human.This creates an object which has
human
as a prototype, which, in turn, hasanimal
as a prototype. Easy enough. No misdirection, no "new object with a prototype equal to the prototype of the function". Just simple prototypal inheritance. It's simple, and straightforward. Polymorphism is easy, too.Due to the way the prototype chain works,
myHuman.speak
will be found inhuman
before it's found inanimal
, and thus our human is a surfer instead of just a boring old animal.So, in conclusion (TLDR):
The pseudo-classical constructor functionality was kind of tacked on to JavaScript to make those programmers trained in classical OOP more comfortable. It is not, by any means, necessary, but it means giving up classical concepts such as member visibility and (tautologically) constructors.
What you get in return is flexibility, and simplicity. You can create "classes" on the fly - every object is, itself, a template for other objects. Setting values on child objects will not affect the prototype of those objects (i.e. if I used
var child = Object.create(myHuman)
, and then setchild.walk = 'not yet'
,animal.walk
would be unaffected - really, test it).The simplicity of inheritance is honestly mind-boggling. I've read a lot on inheritance in JavaScript, and written many lines of code attempting to understand it. But it really boils down to objects inherit from other objects. It's as simple as that, and all the
new
keyword does is muddle that up.This flexibility is difficult to use to its fullest extent, and I'm sure I have yet to do it, but it's there, and it's interesting to navigate. I think most of the reason that it hasn't been used for a large project is that it simply isn't understood as well as it could be, and, IMHO, we're locked into the classical inheritance patterns we all learned when we were taught C++, Java, etc.
Edit
I think I've made a pretty good case against constructors. But my argument against factories is fuzzy.
After further contemplation, during which I've flip-flopped to both sides of the fence several times, I have come to the conclusion that factories are also unnecessary. If
animal
(above) were given another functioninitialize
, it would be trivial to create and initialize a new object that inherits fromanimal
.New object, initialized and ready for use.
@Raynos - You totally nerd sniped me on this one. I should be getting ready for 5 days of doing absolutely nothing productive.
根据您的评论,问题主要是“构造函数知识是否必要?”我觉得是的。
一个简单的例子是存储部分数据。在内存中给定的数据集上,当持久化时,我可能只选择存储某些元素(要么是为了效率,要么是为了数据一致性的目的,例如,一旦持久化,这些值本质上就没用了)。让我们进行一个会话,我在其中存储用户名和他们单击帮助按钮的次数(由于缺乏更好的示例)。当我在示例中坚持这一点时,我确实没有使用点击次数,因为我现在将其保存在内存中,下次加载数据时(下次用户登录或连接或其他任何情况)我将初始化值从头开始(大概为 0)。这个特殊的用例是构造函数逻辑的一个很好的候选者。
啊,但是您始终可以将其嵌入到静态原型中:
Object.create({name:'Bob', clicks:0});
当然,在本例中。但是,如果该值一开始并不总是 0,而是需要计算的值,该怎么办?嗯,比如说,用户的年龄以秒为单位(假设我们存储了姓名和出生日期)。同样,一项几乎没有什么用处的持久性,因为无论如何它都需要在检索时重新计算。那么如何在静态原型中存储用户的年龄呢?显而易见的答案是构造函数/初始化程序逻辑。
还有更多场景,尽管我不认为这个想法与 js oop 或任何特定语言有太大关系。实体创建逻辑的必要性是我所看到的计算机系统模拟世界的方式所固有的。有时我们存储的项目将是简单的检索并注入到原型 shell 等蓝图中,有时值是动态的,需要初始化。
更新
好的,我将尝试一个更真实的示例,为了避免混淆,假设我没有数据库并且不需要保留任何数据。假设我正在制作一个纸牌服务器。每个新游戏(自然)将是
Game
原型的一个新实例。我很清楚,这里需要它们的初始化程序逻辑(以及很多逻辑):例如,我在每个游戏实例上不仅需要静态/硬编码的牌组,而且还需要随机洗牌的牌组。如果它是静态的,用户每次都会玩相同的游戏,这显然不好。
如果玩家用完,我可能还必须启动计时器来完成游戏。同样,不是静态的东西,因为我的游戏有一些要求:秒数与连接的玩家迄今为止赢得的游戏数量成反比(同样,没有保存的信息,只是此连接的数量) ,与洗牌的难度成正比(有一种算法可以根据洗牌的结果来决定游戏的难度)。
如何使用静态
Object.create()
来做到这一点?As per your comment that the question is mainly "is constructor knowledge necessary?" I feel it is.
A toy example would be storing partial data. On a given data set in memory, when persisting I may only choose to store certain elements (either for the sake of efficiency or for data consistency purposes, e.g. the values are inherently useless once persisted). Let's take a session where I store the user name and the number of times they've clicked on the help button (for lack of a better example). When I persist this in my example, I do have no use for the number of clicks, since I keep it in memory now, and next time I load the data (next time the user logs in or connects or whatever) I will initialise the value from scratch (presumably to 0). This particular use case is a good candidate for constructor logic.
Aahh, but you could always just embed that in the static prototype:
Object.create({name:'Bob', clicks:0});
Sure, in this case. But what if the value wasn't always 0 at first, but rather it was something that required computation. Uummmm, say, the users age in seconds (assuming we stored the name and the DOB). Again, an item that there is little use persisting, since it will need to be recalculated on retrieval anyway. So how do you store the user's age in the static prototype?The obvious answer is constructor/initialiser logic.
There are many more scenarios, although I don't feel the idea is much related to js oop or any language in particular. The necessity for entity creation logic is inherent in the way I see computer systems model the world. Sometimes the items we store will be a simple retrieval and injection into a blueprint like prototype shell, and sometimes the values are dynamic, and will need to be initialised.
UPDATE
OK, I'm going to try for a more real-world example, and to avoid confusion assume that I have no database and need not persist any data. Let's say I'm making a solitaire server. Each new game will be (naturally) a new instance of the
Game
prototype. It is clear to me that their is initialiser logic required here (and lots of it):I will, for example, need on each game instance not just a static/hard-coded deck of cards, but a randomly shuffled deck. If it were static the user would play the same game every time, which is clearly not good.
I may also have to start a timer to finish the game if the player runs out. Again, not something that can be static, since my game has a few requirements: the number of seconds is inversely related to the number of games the connected player has won so far (again, no saved info, just how many for this connection), and proportional to the difficulty of the shuffle (there is an algorithm that according to the shuffle results can determine the degree of difficulty of the game).
How do you do that with a static
Object.create()
?可静态克隆“类型”的示例:
现在,我们可以在其他地方克隆此类型,是吗?当然,我们需要使用 var myType = Object.Create(MyType) ,但是这样就完成了,是吗?现在我们只需
myType.size
即可,这就是物体的大小。或者我们可以读取颜色,或者更改它等等。我们还没有创建构造函数或任何东西,对吧?如果你说那里没有构造函数,那你就错了。让我向您展示构造函数在哪里:
因为我们已经创建了我们想要的所有内容,并且已经定义了所有内容。这就是构造函数所做的全部工作。因此,即使我们只克隆/使用静态事物(这就是我在上面的代码片段中看到的),我们仍然有一个构造函数。只是一个静态构造函数。通过定义类型,我们定义了构造函数。另一种方法是这种对象构造模型:
但最终您将想要使用 Object.Create(MyType),并且当您这样做时,您将使用静态对象来创建目标对象。然后就和前面的例子一样了。
Example of a staticly-clonable "Type":
Now, we could clone this type elsewhere, yes? Sure, we would need to use
var myType = Object.Create(MyType)
, but then we're done, yes? Now we can justmyType.size
and that is the size of the thing. Or we could read the color, or change it, etc. We haven't created a constructor or anything, right?If you said there's no constructor there, you're wrong. Let me show you where the constructor is:
Because we've already gone and created all the things we wanted and we've already defined everything. That's all a constructor does. So even if we only clone/use static things (which is what I see the above snippets as doing) we've still had a constructor. Just a static constructor. By defining a type, we have defined a constructor. The alternative is this model of object construction:
But eventually you're going to want to use Object.Create(MyType) and when you do, you will have used a static object to create the target object. And then it becomes the same as the previous example.
对您的问题“我们是否需要原型 OO 中的工厂/构造函数?”的简短回答是没有。工厂/构造函数仅服务于 1 个目的:将新创建的对象(实例)初始化为特定状态。
话虽这么说,它经常被使用,因为某些对象需要某种初始化代码。
让我们使用您提供的基于组件的实体代码。典型的实体只是组件和几个属性的集合:
现在以下代码将基于“BaseEntity”创建新实体
看起来很简单,直到您去引用属性:
输出:
那么这是为什么呢?答案很简单:因为基于原型的继承。当我们创建对象时,没有任何代码可以在实际实例上设置属性
id
和createdTime
,就像构造函数/工厂中通常所做的那样。因此,当访问该属性时,它会从原型链中提取,最终成为所有实体的单个值。其论据是应该向 Object.create() 传递第二个参数来设置该值。我的回答很简单:这不是与调用构造函数或使用工厂基本相同吗?这只是设置对象状态的另一种方式。
现在,在您的实现中,您将所有原型视为(理应如此)静态方法和属性的集合,您可以通过将属性值分配给来自数据源的数据来初始化对象。它可能没有使用 new 或某种类型的工厂,但它是初始化代码。
总结一下:
在 JavaScript 原型 OOP 中
- 不需要
new
- 不需要工厂
- 通常需要初始化代码,这通常是通过 new、工厂或其他一些您不想承认正在初始化对象的实现来完成的
The short answer to your question "Do we need factories/constructors in prototypical OO?" is no. Factories/Constructors serve 1 purpose only: initialize the newly created object (an instance) to a specific state.
That being said, it is often uses because some objects need initialization code of some sort.
Let's use the component-based entity code you provided. A typical entity is simply a collection of components and few properties:
Now the following code will create new entities based on the 'BaseEntity'
Seems straight forward enough, until you go to reference the properties:
outputs:
So why is this? The answer is simple: because of prototype based inheritance. When we created the objects, there wasn't any code to set the properties
id
andcreatedTime
on the actual instance, as is normally done in constructors/factories. As a result, when the property is accessed, it pulls from the prototype chain, which ends up being a single value for all entities.The argument to this is that the Object.create() should be passed the second parameter to set this values. My response would simply be: Isn't that basically the same as calling a constructor or using a factory? It's just another way of setting an object's state.
Now with your implementation where you treat (and rightfully so) all prototypes as a collection of static methods and properties, you do initialize the object by assigning the values of the properties to the data from a data source. It may not be using
new
or some type of factory, but it is initialization code.To summarize:
In JavaScript prototype OOP
-
new
is not needed- Factories are not needed
- Initialization code is usually needed, which is normally done through
new
, factories, or some other implementation that you don't want to admit is initializing an object