For more on the disadvantages of classical inheritance, watch “Classical Inheritance is Obsolete: How to Think in Prototypal OO”:
Delegation / Differential Inheritance
A delegate prototype is an object that serves as a base for another object. When you inherit from a delegate prototype, the new object gets a reference to the prototype.
When you try to access a property on the new object, it checks the object’s own properties first. If it doesn’t find it there, it checks the `[[Prototype]]` , and so on up the prototype chain until it gets back to `Object.prototype` , which is the root delegate for most objects.
Due to the many problems associated with classical inheritance, and the temptation to extend classes, I don’t recommend this technique. I present it here only because it’s likely to be a familiar point of reference.
You may also be familiar with the ES5 constructor function version:
You can avoid property delegation by setting the prototype to `null` using `Object.create(null)`.
One major drawback to delegation is that it’s not very good for storing state. If you try to store state as objects or arrays, mutating any member of the object or array will mutate the member for every instance that shares the prototype. In order to preserve instance safety, you need to make a copy of the state for each object.
Concatenative Inheritance / Cloning / Mixins
Cloning a great way to store default state for objects: This process is commonly achieved using `Object.assign()`. Prior to ES5, it was common to use similar `.extend()` methods from Lodash, Underscore, or jQuery.
It’s common to see this style used for mixins. For example, you can turn any object into an event emitter by mixing in an `EventEmitter3` prototype:
We can use this to create a Backbone-style event emitting model:
Concatenative inheritance is very powerful, but it gets even better when you combine it with closures.
Functions created for the purpose of extending existing objects are commonly referred to as functional mixins. The primary advantage of using functions for extension is that it allows you to use the function closure to encapsulate private data. In other words, you can enforce private state.
It’s a bit awkward to hang the attributes on a public property where a user could set or get them without calling the proper methods. What we really want to do is hide the attributes in a private closure (See also: “What is a Closure?” ) . It looks something like this:
Note in the example above, we have the `mixinModel()` wrapper around the actual functional mixin, `rawMixin()` . The reason we need that is because we need to set the value of `this` inside the function, which we do with `Function.prototype.call()`. We could skip the wrapper and let callers do that instead, but that would be obnoxious.
Composition Over Class Inheritance
“Favor object composition over class inheritance.” ~ The Gang of Four, “ Design Patterns: Elements of Reusable Object Oriented Software ”
Classical inheritance creates is-a relationships with restrictive taxonomies, all of which are eventually wrong for new use-cases. But it turns out, we usually employ inheritance for has-a , uses-a , or can-do relationships.
Composition is more like a guitar effects pedalboard. Want something that can do delay, subtle distortion, and a robot voice? No problem! Just plug them all in:
const effect = compose(delay, distortion, robovoice); // Rock on!
When would you want to use class inheritance? For me, the answer is simple: “Never.”
- More expressive
- More flexible
Watch “Composition Over Inheritance” for more.
The idea seemed to resonate with people, and it wasn’t long before spinoff libraries started to emerge. Notably, Tim Routowicz wrote the excellent react-stamp library for composable React components. We needed a standard to make our implementations interoperable.
The result was The Stamp Specification .
A stamp is a composable factory function that returns object instances based on its descriptor .
Stamps have a method called `.compose()`:
When called the `.compose()` method creates new stamp using the current stamp as a base, composed with a list of composables passed as arguments:
The `.compose()` method doubles as the stamp’s descriptor . In other words, descriptor properties are attached to the stamp `.compose()` method, e.g. `stamp.compose.methods` .
Composable descriptor(or just descriptor ) is a meta data object which contains the information necessary to create an object instance.
A descriptor contains:
- `methods` — A set of methods that will be added to the object’s delegate prototype.
- `properties` — A set of properties that will be added to new object instances by assignment.
- `initializers` — An array of functions that will run in sequence. Stamp details and arguments get passed to initializers.
- `staticProperties` — A set of static properties that will be copied by assignment to the stamp .
There are several other descriptor properties, but these are the essentials that you’ll use most frequently. See the descriptor specification for the full set.
Let’s answer both of these questions at the same time using `init()` and `compose()` from the `stamp-utils` library.
- `compose(…composables: […Composable]) => Stamp` takes any number of composables and returns a new stamp.
- `init(…functions: […Function]) => Stamp` takes any number of initializer functions and returns a new stamp.
First, we’ll use a closure to create data privacy:
It uses function scope to encapsulate private data. Note that the getter must be defined inside the function in order to access the closure variables.
Those `a` ’s are not typos. The point is to demonstrate that `a` and `b` ’s private variables won’t clash.
But here’s the real treat:
WAT? Yeah. You just inherited privileged methods and private data from two sources at the same time.
But that’s boring. Let’s see what else is on tap:
What About `class`?
I’ve been challenging people to come up with a compelling use-case for class inheritance for several years, and instead of good use-cases, I hear a whole lot of common misconceptions .
The examples for this article were all written using `stamp-utils` . It provides a minimal implementation of the `compose()` function, along with an `init()` function, which is sugar for:
The next time you need to build stateful objects with many instances, give stamps a try.
转载本站任何文章请注明：转载至神刀安全网，谢谢神刀安全网 » 3 Different Kinds of Prototypal Inheritance: ES6+ Edition