I’ve been working with JavaScript on and off since the late nineties. I didn’t really like it at first, but after the introduction of ES2015 (aka ES6), I began to appreciate JavaScript as an outstanding, dynamic programming language with enormous, expressive power.
从90年代末开始,我就一直在使用JavaScript。 一开始我并不真正喜欢它,但是在引入ES2015(又名ES6)之后,我开始欣赏JavaScript作为一种出色的动态编程语言,具有巨大的表达能力。
Over time, I’ve adopted several coding patterns that have lead to cleaner, more testable, more expressive code. Now, I am sharing these patterns with you.
随着时间的流逝,我采用了几种编码模式,这些编码模式导致了更简洁,更可测试,更具表现力的代码。 现在,我正在与您分享这些模式。
I wrote about the first pattern — “RORO” — in the article below. Don’t worry if you haven’t read it, you can read these in any order.
我在下面的文章中介绍了第一种模式-“ RORO”。 如果您还没有阅读,请不要担心,您可以按任何顺序阅读它们。
Elegant patterns in modern JavaScript: ROROI wrote my first few lines of JavaScript not long after the language was invented. If you told me at the time that I…medium.freecodecamp.org
现代JavaScript中的优雅模式:RORO 在发明该语言后不久,我就编写了前几行JavaScript。 如果您当时告诉我我... medium.freecodecamp.org
Today, I’d like to introduce you to the “Ice Factory” pattern.
今天,我想向您介绍“制冰厂”模式。
An Ice Factory is just a function that creates and returns a frozen object. We’ll unpack that statement in a moment, but first let’s explore why this pattern is so powerful.
制冰厂只是一个创建和返回冻结对象的函数 。 稍后我们将解压缩该语句,但首先让我们探讨一下为什么这种模式如此强大。
JavaScript类不是那么优雅 (JavaScript classes are not so classy)
It often makes sense to group related functions into a single object. For example, in an e-commerce app, we might have a cart
object that exposes an addProduct
function and a removeProduct
function. We could then invoke these functions with cart.addProduct()
and cart.removeProduct()
.
将相关功能分组到单个对象中通常很有意义。 例如,在一个电子商务应用程序中,我们可能有一个cart
对象,该对象公开了addProduct
函数和removeProduct
函数。 然后,我们可以使用cart.addProduct()
和cart.removeProduct()
调用这些函数。
If you come from a Class-centric, object oriented, programming language like Java or C#, this probably feels quite natural.
如果您来自以Java为中心的面向对象的,面向对象的编程语言(例如Java或C#),那可能会感觉很自然。
If you’re new to programming — now that you’ve seen a statement like cart.addProduct()
. I suspect the idea of grouping together functions under a single object is looking pretty good.
如果您不cart.addProduct()
编程,那么现在您已经看到了类似cart.addProduct()
的语句。 我怀疑在单个对象下将功能分组在一起的想法看起来不错。
So how would we create this nice little cart
object? Your first instinct with modern JavaScript might be to use a class
. Something like:
那么,我们将如何创建这个漂亮的小cart
对象? 您对现代JavaScript的本能可能是使用class
。 就像是:
// ShoppingCart.js
export default class ShoppingCart { constructor({db}) { this.db = db } addProduct (product) { this.db.push(product) } empty () { this.db = [] }
get products () { return Object .freeze([...this.db]) }
removeProduct (id) { // remove a product }
// other methods
}
// someOtherModule.js
const db = [] const cart = new ShoppingCart({db})cart.addProduct({ name: 'foo', price: 9.99})
Note: I’m using an Array for the
db
parameter for simplicity’s sake. In real code this would be something like a Model or Repo that interacts with an actual database.
Unfortunately — even though this looks nice — classes in JavaScript behave quite differently from what you might expect.
不幸的是,尽管看起来不错,但是JavaScript中的类的行为与您期望的完全不同。
JavaScript Classes will bite you if you’re not careful.
如果您不小心,JavaScript类会咬您。
For example, objects created using the new
keyword are mutable. So, you can actually re-assign a method:
例如,使用new
关键字创建的对象是可变的。 因此,您实际上可以重新分配一个方法:
const db = []const cart = new ShoppingCart({db})
cart.addProduct = () => 'nope!' // No Error on the line above!
cart.addProduct({ name: 'foo', price: 9.99}) // output: "nope!" FTW?
Even worse, objects created using the new
keyword inherit the prototype
of the class
that was used to create them. So, changes to a class’ prototype
affect all objects created from that class
— even if a change is made after the object was created!
更糟糕的是,使用new
关键字创建的对象将继承用于创建它们的class
的prototype
。 因此,对类prototype
更改会影响从class
创建的所有对象-即使在创建对象之后进行了更改!
Look at this:
看这个:
const cart = new ShoppingCart({db: []})const other = new ShoppingCart({db: []})
ShoppingCart.prototype .addProduct = () => ‘nope!’// No Error on the line above!
cart.addProduct({ name: 'foo', price: 9.99}) // output: "nope!"
other.addProduct({ name: 'bar', price: 8.88}) // output: "nope!"
Then there's the fact that this
In JavaScript is dynamically bound. So, if we pass around the methods of our cart
object, we can lose the reference to this
. That’s very counter-intuitive and it can get us into a lot of trouble.
然后事实是, this
In JavaScript是动态绑定的。 因此,如果我们传递cart
对象的方法,则可能会丢失this
的引用。 这是违反直觉的,并且可能使我们陷入很多麻烦。
A common trap is assigning an instance method to an event handler.
一个常见的陷阱是将实例方法分配给事件处理程序。
Consider our cart.empty
method.
考虑我们的cart.empty
方法。
empty () { this.db = [] }
If we assign this method directly to the click
event of a button on our web page…
如果我们直接将此方法分配给网页上按钮的click
事件,则……
<button id="empty"> Empty cart</button>
---
document .querySelector('#empty') .addEventListener( 'click', cart.empty )
… when users click the empty button
, their cart
will remain full.
…当用户单击空button
,他们的cart
将保持满满。
It fails silently because this
will now refer to the button
instead of the cart
. So, our cart.empty
method ends up assigning a new property to our button
called db
and setting that property to []
instead of affecting the cart
object’s db
.
它会静默失败,因为this
现在将引用button
而不是cart
。 因此,我们的cart.empty
方法最终为button
db
分配了一个新属性,并将该属性设置为[]
而不影响cart
对象的db
。
This is the kind of bug that will drive you crazy because there is no error in the console and your common sense will tell you that it should work, but it doesn’t.
这种错误会使您发疯,因为控制台中没有错误,并且您的常识将告诉您它应该起作用,但事实并非如此。
To make it work we have to do:
为了使其工作,我们必须做:
document .querySelector("#empty") .addEventListener( "click", () => cart.empty() )
Or:
要么:
document .querySelector("#empty") .addEventListener( "click", cart.empty.bind(cart) )
I think Mattias Petter Johansson said it best:
我认为Mattias Petter Johansson 说得最好 :
“
new
andthis
[in JavaScript] are some kind of unintuitive, weird, cloud rainbow trap.”“
new
和this
[在JavaScript]是某种直观的,怪异的,云彩虹陷阱。”
制冰厂营救 (Ice Factory to the rescue)
As I said earlier, an Ice Factory is just a function that creates and returns a frozen object. With an Ice Factory our shopping cart example looks like this:
如前所述, Ice Factory只是一个创建并返回冻结对象的函数 。 对于制冰厂,我们的购物车示例如下所示:
// makeShoppingCart.js
export default function makeShoppingCart({ db}) { return Object.freeze({ addProduct, empty, getProducts, removeProduct, // others })
function addProduct (product) { db.push(product) } function empty () { db = [] }
function getProducts () { return Object .freeze([...db]) }
function removeProduct (id) { // remove a product }
// other functions}
// someOtherModule.js
const db = []const cart = makeShoppingCart({ db })cart.addProduct({ name: 'foo', price: 9.99})
Notice our “weird, cloud rainbow traps” are gone:
注意我们的“奇怪的云彩虹陷阱”已经消失了:
We no longer need
new
.我们不再需要
new
。We just invoke a plain old JavaScript function to create our
我们只是调用一个普通的旧JavaScript函数来创建我们的
cart
object.cart
对象。We no longer need
this
.我们不再需要
this
。We can access the
我们可以访问
db
object directly from our member functions.db
对象直接来自我们的成员函数。Our
cart
object is completely immutable.我们的
cart
对象是完全不变的。Our
cart
object is completely immutable.Object.freeze()
freezes thecart
object so that new properties can’t be added to it, existing properties can’t be removed or changed, and the prototype can’t be changed either. Just remember thatObject.freeze()
is shallow, so if the object we return contains anarray
or anotherobject
we must make sure toObject.freeze()
them as well. Also, if you’re using a frozen object outside of an ES Module, you need to be in strict mode to make sure that re-assignments cause an error rather than just failing silently.我们的
cart
对象是完全不变的。Object.freeze()
冻结cart
对象,以便不能向其添加新属性,也不能删除或更改现有属性,也不能更改原型。 只要记住Object.freeze()
是浅层的 ,所以如果我们返回的对象包含一个array
或另一个object
我们必须确保也将它们对Object.freeze()
。 另外,如果您使用的是ES Module之外的冻结对象,则需要处于严格模式下 ,以确保重新分配会导致错误,而不仅仅是静默失败。
请注意一点隐私 (A little privacy please)
Another advantage of Ice Factories is that they can have private members. For example:
冰工厂的另一个优势是他们可以拥有私人成员。 例如:
function makeThing(spec) { const secret = 'shhh!'
return Object.freeze({ doStuff })
function doStuff () { // We can use both spec // and secret in here }}
// secret is not accessible out here
const thing = makeThing()thing.secret // undefined
This is made possible because of Closures in JavaScript, which you can read more about on MDN.
由于JavaScript中的Closures,使之成为可能,您可以在MDN上了解更多信息。
请一点感谢 (A little acknowledgement please)
Although Factory Functions have been around JavaScript forever, the Ice Factory pattern was heavily inspired by some code that Douglas Crockford showed in this video.
尽管Factory Functions永远都围绕着JavaScript进行,但是Ice Factory模式在很大程度上受到了Douglas Crockford在此视频中显示的一些代码的启发。
Here’s Crockford demonstrating object creation with a function he calls “constructor”:
这是克罗克福德(Crockford)展示的对象创建过程,他称之为“构造函数”:
My Ice Factory version of the Crockford example above would look like this:
上面的Crockford示例的Ice Factory版本如下所示:
function makeSomething({ member }) { const { other } = makeSomethingElse() return Object.freeze({ other, method })
function method () { // code that uses "member" }}
I took advantage of function hoisting to put my return statement near the top, so that readers would have a nice little summary of what’s going on before diving into the details.
我利用函数提升将我的return语句放在顶部附近,以便读者在深入了解细节之前可以对所发生的事情有一个很好的总结。
I also used destructuring on the spec
parameter. And I renamed the pattern to “Ice Factory” so that it’s more memorable and less easily confused with the constructor
function from a JavaScript class
. But it’s basically the same thing.
我还对spec
参数使用了解构。 然后,我将模式重命名为“ Ice Factory”,以使其更令人难忘,并且不易与JavaScript class
的constructor
函数混淆。 但这基本上是同一回事。
So, credit where credit is due, thank you Mr. Crockford.
所以,只要有信用就可以信用,谢谢克罗克福德先生。
Note: It’s probably worth mentioning that Crockford considers function “hoisting” a “bad part” of JavaScript and would likely consider my version heresy. I discussed my feelings on this in a previous article and more specifically, this comment.
注意: 可能值得一提的是Crockford将函数“提升”为JavaScript的“不良部分”,并可能将我的版本视为异端。 我在上一篇文章中 ,特别是在评论中 ,讨论了对此的感受。
那继承呢? (What about inheritance?)
If we tick along building out our little e-commerce app, we might soon realize that the concept of adding and removing products keeps cropping up again and again all over the place.
如果我们逐步构建自己的小型电子商务应用程序,我们可能很快就会意识到添加和删除产品的概念不断在整个地方反复出现。
Along with our Shopping Cart, we probably have a Catalog object and an Order object. And all of these probably expose some version of `addProduct` and `removeProduct`.
除了购物车,我们可能还有一个Catalog对象和一个Order对象。 所有这些可能都暴露了`addProduct`和`removeProduct`的某些版本。
We know that duplication is bad, so we’ll eventually be tempted to create something like a Product List object that our cart, catalog, and order can all inherit from.
我们知道复制是不好的,所以我们最终会被诱惑创建类似产品清单对象的东西,而我们的购物车,目录和订单都可以从中继承。
But rather than extending our objects by inheriting a Product List, we can instead adopt the timeless principle offered in one of the most influential programming books ever written:
但是,除了继承继承产品列表来扩展对象之外,我们还可以采用有史以来最有影响力的编程书籍之一中提供的永恒原理:
“Favor object composition over class inheritance.”
“在类继承上的主要对象组成。”
In fact, the authors of that book — colloquially known as “The Gang of Four” — go on to say:
实际上,这本书的作者(俗称“四人帮”)继续说:
“…our experience is that designers overuse inheritance as a reuse technique, and designs are often made more reusable (and simpler) by depending more on object composition.”
“……我们的经验是,设计师将继承作为一种重用技术而过度使用,并且通过更多地依赖于对象的组成,设计通常变得更可重用(和更简单)。”
So, here’s our product list:
因此,这是我们的产品列表:
function makeProductList({ productDb }) { return Object.freeze({ addProduct, empty, getProducts, removeProduct, // others )} // definitions for // addProduct, etc…}
And here’s our shopping cart:
这是我们的购物车:
function makeShoppingCart(productList) { return Object.freeze({ items: productList, someCartSpecificMethod, // …)}
function someCartSpecificMethod () { // code }}
And now we can just inject our Product List into our Shopping Cart, like this:
现在,我们可以将产品列表插入购物车中,如下所示:
const productDb = []const productList = makeProductList({ productDb })
const cart = makeShoppingCart(productList)
And use the Product List via the `items` property. Like:
并通过“ items”属性使用“产品列表”。 喜欢:
cart.items.addProduct()
It may be tempting to subsume the entire Product List by incorporating its methods directly into the shopping cart object, like so:
通过将其方法直接合并到购物车对象中,可能会试图包含整个产品列表,如下所示:
function makeShoppingCart({ addProduct, empty, getProducts, removeProduct, …others}) { return Object.freeze({ addProduct, empty, getProducts, removeProduct, someOtherMethod, …others)}
function someOtherMethod () { // code }}
In fact, in an earlier version of this article, I did just that. But then it was pointed out to me that this is a bit dangerous (as explained here). So, we’re better off sticking with proper object composition.
实际上,在本文的早期版本中,我只是这样做的。 但后来有人向我指出,这是一个有点危险(如解释在这里 )。 因此,我们最好坚持适当的对象组成。
Awesome. I’m Sold!
太棒了 我卖了!
Whenever we’re learning something new, especially something as complex as software architecture and design, we tend to want hard and fast rules. We want to hear thing like “always do this” and “ never do that.”
每当我们学习新的东西,尤其是像软件体系结构和设计这样复杂的东西时,我们都倾向于要求严格而快速的规则。 我们希望听到“ 永远做”和“ 永远不做”的事情。
The longer I spend working with this stuff, the more I realize that there’s no such thing as always and never. It’s about choices and trade-offs.
我花在这个东西上的时间越长,我越意识到没有永远存在的东西。 这与选择和权衡有关。
Making objects with an Ice Factory is slower and takes up more memory than using a class.
与使用类相比,使用Ice Factory制作对象要慢,并且占用更多内存。
In the types of use case I’ve described, this won’t matter. Even though they are slower than classes, Ice Factories are still quite fast.
在我所描述的用例类型中,这无关紧要。 尽管它们比课堂要慢,但是冰工厂仍然相当快。
If you find yourself needing to create hundreds of thousands of objects in one shot, or if you’re in a situation where memory and processing power is at an extreme premium you might need a class instead.
如果您发现自己需要一次创建数十万个对象,或者您的内存和处理能力极其昂贵,则可能需要一个类。
Just remember, profile your app first and don’t prematurely optimize. Most of the time, object creation is not going to be the bottleneck.
请记住,请先配置您的应用程序,然后过早进行优化。 大多数情况下,对象创建不会成为瓶颈。
Despite my earlier rant, Classes are not always terrible. You shouldn’t throw out a framework or library just because it uses classes. In fact, Dan Abramov wrote pretty eloquently about this in his article, How to use Classes and Sleep at Night.
尽管我以前很吵,但上课并不总是那么糟糕。 您不应该仅仅因为框架或库使用了类就扔掉它。 实际上, 丹·阿布拉莫夫 ( Dan Abramov )在他的文章“ 如何使用班级和夜间睡眠”中雄辩地写道。
Finally, I need to acknowledge that I’ve made a bunch of opinionated style choices in the code samples I’ve presented to you:
最后,我需要承认,在提供给您的代码示例中,我已经做出了许多自以为是的样式选择:
- I put my return statement near the top (this is made possible by my use of function statements, see above). 我将return语句放在顶部附近(这可以通过使用函数语句来实现,请参见上文)。
I name my factory function,
makeX
instead ofcreateX
orbuildX
or something else.我将工厂函数
makeX
为makeX
而不是createX
或buildX
或其他名称。My factory function takes a single, destructured, parameter object.
I don’t use semi-colons (Crockford would also NOT approve of that)
我不使用分号( Crockford也不会赞成 )
- and so on… 等等…
You may make different style choices, and that’s okay! The style is not the pattern.
您可以选择不同的样式, 这没关系 ! 样式不是样式。
The Ice Factory pattern is just: use a function to create and return a frozen object. Exactly how you write that function is up to you.
Ice Factory模式就是: 使用函数创建并返回冻结的对象 。 具体如何编写该函数取决于您。
If you’ve found this article useful, please smash that applause icon a bunch of times to help spread the word. And if you want to learn more stuff like this, please sign up for my Dev Mastery newsletter below. Thanks!
如果您发现这篇文章很有用,请捣毁该掌声图标多次,以帮助您宣传。 如果您想了解更多类似的内容,请在下面注册我的开发精通通讯。 谢谢!
2019年更新:这是我经常使用此模式的视频! (UPDATE 2019: Here’s a video where I use this pattern, a lot!)
翻译自: https://www.freecodecamp.org/news/elegant-patterns-in-modern-javascript-ice-factory-4161859a0eee/