神刀安全网

JavaScript Factory Functions vs Constructor Functions vs Classes

Prior to ES6, there was a lot of confusion about the differences between a factory function and a constructor function in JavaScript. Since ES6 has the `class` keyword, a lot of people seem to think that solved many problems with constructor functions. It didn’t. Let’s explore the major differences you still need to be aware of.

First, let’s look at examples of each:

Each of these strategies stores methods on a shared prototype, and optionally supports private data via constructor function closures. In other words, they have mostly the same features, and could mostly be used interchangeably.

In JavaScript, any function can return a new object. When it’s not a constructor function or class, it’s called a factory function.

ES6 classes desugar to constructor functions, so everything that follows about constructor functions also applies to ES6 classes:

class Foo {}
console.log(typeof Foo); // function

Benefits of Constructors & `class`

  • Most books teach you to use class or constructors.
  • `this` refers to the new object.
  • Some people like the way `myFoo = new Foo()` reads.
  • There may be a micro-optimization performance benefit, but you should not worry about that unless you have profiled your code and proven that it’s an issue for you.

Drawbacks of Constructors & `class`

1. Required `new`.

Prior to ES6, forgetting `new` was a very common bug. To counter it, many people used boilerplate to enforce it:

function Foo() {
if (!(this instanceof Foo)) { return new Foo(); }
}

In ES6 (ES2015) if you try to call a class constructor without new, it will always throw an error. It’s not possible to avoid forcing the new requirement on callers without wrapping your class in a factory function. There is a proposal for a future version of JavaScript to allow customizing the call behavior when callers omit `new` , but the proposal adds overhead to every class that uses it (meaning few will use it).

2. Details of instantiation get leaked into the calling API (via the `new` requirement).

All callers are tightly coupled to the constructor implementation. If you ever need the additional flexibility of the factory, the refactor is a breaking change . Class to factory refactors are common enough that they appear in the seminal Refactoring book, “Refactoring: Improving the Design of Existing Code” by Martin Fowler, Kent Beck, John Brant, William Opdyke, and Don Roberts.

3. Constructors break the Open / Closed Principle

Because of the `new` requirement, constructor functions violate the open/closed principle: an API should be open for extension, but closed for modification.

I argue that the class to factory refactor is common enough that it should be considered a standard extension for all constructors: Upgrading from a class to a factory should not break things.

If you start out exporting a constructor or class and users start using the constructor, then down the road you realize you need the flexibility of a factory, (for instance, to switch the implementation to use object pools, or to instantiate across execution contexts, or to have more inheritance flexibility using alternative prototypes), you can’t do so without potentially forcing a refactor on callers.

Unfortunately, in JavaScript, switching from a constructor or class to a factory is a breaking change :

In the example above, we start out with a class, but we want to add the capability to offer different kinds of car bundles. To do so, the factory uses alternative prototypes for different car bundles. I’ve used this technique to store various implementations of a media player interface, picking the correct prototype based on the type of media the player needed to control.

4. Using Constructors Enables the Deceptive `instanceof`

One of the breaking changes in the constructor to factory refactor is `instanceof`. Sometimes people are tempted to use `instanceof` as a type check guard in their code. That can be very problematic. I recommend that you avoid `instanceof`.

`instanceof` lies.

`instanceof` does not do type checking the way that you expect similar checks to do in strongly typed languages. Instead, it does an identity check comparing the object’s `[[Prototype]]` object to the `Constructor.prototype` property.

It won’t work across execution contexts, for example (a common source of bugs, frustration, and unnecessary limitations). It also doesn’t work if your `Constructor.prototype` gets replaced.

It will also fail if you start out with a class or constructor (which returns `this` , linked to the `Constructor.prototype` ), and then switch to exporting an arbitrary object (not linked to the `Constructor.prototype` ), which is what happens when you change from a constructor to a factory.

In short, `instanceof` is another way in which switching from a constructor to a factory is a breaking change .

Benefits of Using class

  • Convenient, self-contained syntax.
  • A single, canonical way to emulate classes in JavaScript. Prior to ES6, there were several competing implementations in popular libraries.
  • More familiar to people from a class-based language background.

Drawbacks of Using class

All of the constructor drawbacks, plus:

  • Temptation for users to create problematic class hierarchies using the extends keyword.

Class hierarchies lead to a bunch of well-known problems in object oriented design, including the fragile base class problem, the gorilla banana problem, the duplication by necessity problem, and so on. Unfortunately, class affords extends like balls afford throwing and chairs afford sitting. For more, read “The Two Pillars of JavaScript: Prototypal OO” and “Inside the Dev Team Death Spiral” .

It’s worth noting that both constructors and factories can also be used to create problematic inheritance hierarchies, but with the `extends` keyword, class creates an affordance that leads you down the wrong path. In other words, it encourages you to think in terms of inflexible (and often wrong) is-a relationships, rather than the more flexible compositional has-a or can-do relationships.

An affordance is a feature that affords the opportunity to perform an certain action. For example, a knob affords twisting, a lever affords pulling, a button affords pressing, etc…

Benefits of Using Factories

Factories are much more flexible than either constructor functions or classes, and they don’t lead people down the wrong path by tempting them with the `extends` keyword and deep inheritance hierarchies. You can inherit from factory functions using a variety of techniques. In particular, check out the Stamp Specification for composable factory functions.

1. Return any arbitrary object and use any arbitrary prototype

For example, you can easily create various types of objects which implement the same API, e.g., a media player that can instantiate players for multiple types of video content which use different APIs under the hood, or an event library which can emit DOM events or web socket events.

Factories can also instantiate objects across execution contexts, take advantage of object pools, and allow for more flexible prototypal inheritance models.

2. No refactoring worries

You’d never have a need to convert from a factory to a constructor, so refactoring will never be an issue.

3. No `new`

No ambiguity about using `new` . Don’t. (It will make `this` behave badly, see next point).

4. Standard `this` behavior

`this` behaves as it normally would, so you can use it to access the parent object. For example, inside `player.create()` , `this` refers to player, just like any other method invocation would. `call()` and `apply()` also reassign `this` as expected.

5. No deceptive `instanceof`

6. Some people like the way `myFoo = createFoo()` reads

Drawbacks of Factories

  • Doesn’t create a link from the instance to ` Factory.prototype` — but this is actually a good thing because you won’t get a deceptive `instanceof`. Instead, `instanceof` will always fail. See benefits.
  • `this` doesn’t refer to the new object inside the factory. See benefits.
  • It may perform slower than a constructor function in micro-optimization benchmarks. Be sure to test in the context of your app if this is a concern for you.

Conclusion

In my opinion, `class` may have a convenient syntax, but that can’t make up for the fact that it lures unwary users to crash on the rocks of class inheritance. It’s also risky because in the future, you may want to upgrade to a factory, but all your callers will be tightly coupled to the constructor function because of the `new` keyword and the fact that moving from classes to factories is a breaking change.

You may be thinking that you can just refactor the call sites, but on large teams, or if the class you’re working with is part of a public API, you could break code that isn’t in your control. In other words, you can’t always assume that refactoring callers is even an option.

The cool thing about factories is that they’re not just more powerful and more flexible, they’re also the easiest way to encourage entire teams, and entire API user bases to use patterns that are simple, flexible, and safe.

There’s a lot more to be said about the power of factory functions, particularly about their utility for object composition using stamps. For more on that topic and how it differs from class inheritance, read “3 Different Kinds of Prototypal Inheritance” .

转载本站任何文章请注明:转载至神刀安全网,谢谢神刀安全网 » JavaScript Factory Functions vs Constructor Functions vs Classes

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址