神刀安全网

3 Different Kinds of Prototypal Inheritance: ES6+ Edition

3 Different Kinds of Prototypal Inheritance: ES6+ Edition
Triplets — Phil Dolby (CC-BY-2.0)

This article was adapted from “Programming JavaScript Applications” , expanded, and updated for ES6.

In order to claim fluency in JavaScript, it’s important to understand how JavaScript’s native inheritance capabilities work. This is an often neglected area of JavaScript writing and learning, but understanding it can be dramatically empowering.

JavaScript is one of the most expressive programming languages ever created. One of it’s best features is the ability to create and inherit from objects without classes and class inheritance.

Its combination of delegate prototypes, runtime object extension, and closures allow you to express three distinct kinds of prototypes in JavaScript, which provide significant advantages over classical inheritance.

For more on the disadvantages of classical inheritance, watch “Classical Inheritance is Obsolete: How to Think in Prototypal OO”:

Recorded at the O’Reilly Fluent conference, 2013

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.

3 Different Kinds of Prototypal Inheritance: ES6+ Edition
The Prototype Chain

Method delegation can preserve memory resources because you only need one copy of each method to be shared by all instances. There are several ways to set up the relationship in JavaScript. The one you’re likely to see in ES6 goes something like this:

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:

I prefer a factory function using `Object.create()`. (In JavaScript, any function can create new objects. When it’s not a constructor function, it’s called a factory function) :

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

Concatenative inheritance is the process of copying the properties from one object to another, without retaining a reference between the two objects. It relies on JavaScript’s dynamic object extension feature.

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.

Functional Inheritance

Coined by Douglas Crockford in “JavaScript: The Good Parts”. Functional inheritance makes use of a factory function, and then tacks on new properties using concatenative inheritance. Not to be confused with functional programming.

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.”

Composition is:

  • Simpler
  • More expressive
  • More flexible

Watch “Composition Over Inheritance” for more.

Stamps

Stamps are composable factory functions . I created stamps as an experiment for the O’Reilly book, “Programming JavaScript Applications” . It all started with this interesting challenge:

There is sugar for faking classes coming to JavaScript (I strongly discourage using it). What would it look like if we created sugar for prototypal OO that supported all of the best features JavaScript has to offer?

The result of that exploration was (as far as I know) the most popular prototype-based inheritance library for JavaScript: Stampit .

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:

A composable is a stamp or a POJO (Plain Old JavaScript Object) stamp descriptor.

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` .

Descriptor

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.

Why Stamps?

Prototypal inheritance is great, and JavaScript’s capabilities give us some really powerful tools to explore it, but it could be easier to use.

Basic questions like “how do I inherit privileged methods and private data?” and “what are some good alternatives to inheritance hierarchies?” are stumpers for many JavaScript users.

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.

Here’s another:

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`?

As you can see, JavaScript provides a very flexible object system without the need to rely on `class`. So why did we add `class` in the first place? Because a lot of people are familiar with the `class` paradigm from other languages, and people kept trying to emulate it in JavaScript.

Inheritance in JavaScript is so easy, it confuses people who expect it to take effort. To make it harder, we added `class`.

Several popular libraries implemented pseudo-classical inheritance in JavaScript using the delegate prototype chain to emulate class inheritance. Adding an official `class` keyword provided a single canonical way to emulate class inheritance in JavaScript — but in my opinion,you should avoid it altogether. In JavaScript, composition is simpler, more expressive, and more flexible than class inheritance. I can’t think of a single good use case where `class` is a better fit than the native prototypal alternatives.

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 .

Conclusion

Once you start thinking in terms of class-free objects and inheritance using prototypes, object composition via concatenation, and stamps, you begin to really appreciate how simple, powerful, and flexible JavaScript’s object system can be.

The stamp community has grown a lot recently. We’ve created a number of stamp-based libraries and utilities. The most used ones are Stampit (the original), react-stamp , and stamp-utils .

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

分享到:更多 ()

评论 抢沙发

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