Benefits of prototypal inheritance over classical?
So I finally stopped dragging my feet all these years and decided to learn JavaScript “properly”. One of the most head-scratching elements of the languages design is its implementation of inheritance. Having experience in Ruby, I was really happy to see closures and dynamic typing; but for the开发者_高级运维 life of me can’t figure out what benefits are to be had from object instances using other instances for inheritance.
I know that this answer is 3 years late but I really think the current answers do not provide enough information about how prototypal inheritance is better than classical inheritance.
First let's see the most common arguments JavaScript programmers state in defence of prototypal inheritance (I'm taking these arguments from the current pool of answers):
- It's simple.
- It's powerful.
- It leads to smaller, less redundant code.
- It's dynamic and hence it's better for dynamic languages.
Now these arguments are all valid, but nobody has bothered explaining why. It's like telling a child that studying Maths is important. Sure it is, but the child certainly doesn't care; and you can't make a child like Maths by saying that it's important.
I think the problem with prototypal inheritance is that it's explained from the perspective of JavaScript. I love JavaScript, but prototypal inheritance in JavaScript is wrong. Unlike classical inheritance there are two patterns of prototypal inheritance:
- The prototypal pattern of prototypal inheritance.
- The constructor pattern of prototypal inheritance.
Unfortunately JavaScript uses the constructor pattern of prototypal inheritance. This is because when JavaScript was created, Brendan Eich (the creator of JS) wanted it to look like Java (which has classical inheritance):
And we were pushing it as a little brother to Java, as a complementary language like Visual Basic was to C++ in Microsoft’s language families at the time.
This is bad because when people use constructors in JavaScript they think of constructors inheriting from other constructors. This is wrong. In prototypal inheritance objects inherit from other objects. Constructors never come into the picture. This is what confuses most people.
People from languages like Java, which has classical inheritance, get even more confused because although constructors look like classes they don't behave like classes. As Douglas Crockford stated:
This indirection was intended to make the language seem more familiar to classically trained programmers, but failed to do that, as we can see from the very low opinion Java programmers have of JavaScript. JavaScript’s constructor pattern did not appeal to the classical crowd. It also obscured JavaScript’s true prototypal nature. As a result, there are very few programmers who know how to use the language effectively.
There you have it. Straight from the horse's mouth.
True Prototypal Inheritance
Prototypal inheritance is all about objects. Objects inherit properties from other objects. That's all there is to it. There are two ways of creating objects using prototypal inheritance:
- Create a brand new object.
- Clone an existing object and extend it.
Note: JavaScript offers two ways to clone an object - delegation and concatenation. Henceforth I'll use the word "clone" to exclusively refer to inheritance via delegation, and the word "copy" to exclusively refer to inheritance via concatenation.
Enough talk. Let's see some examples. Say I have a circle of radius 5
:
var circle = {
radius: 5
};
We can calculate the area and the circumference of the circle from its radius:
circle.area = function () {
var radius = this.radius;
return Math.PI * radius * radius;
};
circle.circumference = function () {
return 2 * Math.PI * this.radius;
};
Now I want to create another circle of radius 10
. One way to do this would be:
var circle2 = {
radius: 10,
area: circle.area,
circumference: circle.circumference
};
However JavaScript provides a better way - delegation. The Object.create
function is used to do this:
var circle2 = Object.create(circle);
circle2.radius = 10;
That's all. You just did prototypal inheritance in JavaScript. Wasn't that simple? You take an object, clone it, change whatever you need to, and hey presto - you got yourself a brand new object.
Now you might ask, "How is this simple? Every time I want to create a new circle I need to clone circle
and manually assign it a radius". Well the solution is to use a function to do the heavy lifting for you:
function createCircle(radius) {
var newCircle = Object.create(circle);
newCircle.radius = radius;
return newCircle;
}
var circle2 = createCircle(10);
In fact you can combine all of this into a single object literal as follows:
var circle = {
radius: 5,
create: function (radius) {
var circle = Object.create(this);
circle.radius = radius;
return circle;
},
area: function () {
var radius = this.radius;
return Math.PI * radius * radius;
},
circumference: function () {
return 2 * Math.PI * this.radius;
}
};
var circle2 = circle.create(10);
Prototypal Inheritance in JavaScript
If you notice in the above program the create
function creates a clone of circle
, assigns a new radius
to it and then returns it. This is exactly what a constructor does in JavaScript:
function Circle(radius) {
this.radius = radius;
}
Circle.prototype.area = function () {
var radius = this.radius;
return Math.PI * radius * radius;
};
Circle.prototype.circumference = function () {
return 2 * Math.PI * this.radius;
};
var circle = new Circle(5);
var circle2 = new Circle(10);
The constructor pattern in JavaScript is the prototypal pattern inverted. Instead of creating an object you create a constructor. The new
keyword binds the this
pointer inside the constructor to a clone of the prototype
of the constructor.
Sounds confusing? It's because the constructor pattern in JavaScript unnecessarily complicates things. This is what most programmers find difficult to understand.
Instead of thinking of objects inheriting from other objects they think of constructors inheriting from other constructors and then become utterly confused.
There's a whole bunch of other reasons why the constructor pattern in JavaScript should be avoided. You can read about them in my blog post here: Constructors vs Prototypes
So what are the benefits of prototypal inheritance over classical inheritance? Let's go through the most common arguments again, and explain why.
1. Prototypal Inheritance is Simple
CMS states in his answer:
In my opinion the major benefit of prototypal inheritance is its simplicity.
Let's consider what we just did. We created an object circle
which had a radius of 5
. Then we cloned it and gave the clone a radius of 10
.
Hence we only need two things to make prototypal inheritance work:
- A way to create a new object (e.g. object literals).
- A way to extend an existing object (e.g.
Object.create
).
In contrast classical inheritance is much more complicated. In classical inheritance you have:
- Classes.
- Object.
- Interfaces.
- Abstract Classes.
- Final Classes.
- Virtual Base Classes.
- Constructors.
- Destructors.
You get the idea. The point is that prototypal inheritance is easier to understand, easier to implement, and easier to reason about.
As Steve Yegge puts it in his classical blog post "Portrait of a N00b":
Metadata is any kind of description or model of something else. The comments in your code are just a a natural-language description of the computation. What makes metadata meta-data is that it's not strictly necessary. If I have a dog with some pedigree paperwork, and I lose the paperwork, I still have a perfectly valid dog.
In the same sense classes are just meta-data. Classes aren't strictly required for inheritance. However some people (usually n00bs) find classes more comfortable to work with. It gives them a false sense of security.
Well, we also know that static types are just metadata. They're a specialized kind of comment targeted at two kinds of readers: programmers and compilers. Static types tell a story about the computation, presumably to help both reader groups understand the intent of the program. But the static types can be thrown away at runtime, because in the end they're just stylized comments. They're like pedigree paperwork: it might make a certain insecure personality type happier about their dog, but the dog certainly doesn't care.
As I stated earlier, classes give people a false sense of security. For example you get too many NullPointerException
s in Java even when your code is perfectly legible. I find classical inheritance usually gets in the way of programming, but maybe that's just Java. Python has an amazing classical inheritance system.
2. Prototypal Inheritance is Powerful
Most programmers who come from a classical background argue that classical inheritance is more powerful than prototypal inheritance because it has:
- Private variables.
- Multiple inheritance.
This claim is false. We already know that JavaScript supports private variables via closures, but what about multiple inheritance? Objects in JavaScript only have one prototype.
The truth is that prototypal inheritance supports inheriting from multiple prototypes. Prototypal inheritance simply means one object inheriting from another object. There are actually two ways to implement prototypal inheritance:
- Delegation or Differential Inheritance
- Cloning or Concatenative Inheritance
Yes JavaScript only allows objects to delegate to one other object. However it allows you to copy the properties of an arbitrary number of objects. For example _.extend
does just this.
Of course many programmers don't consider this to be true inheritance because instanceof
and isPrototypeOf
say otherwise. However this can be easily remedied by storing an array of prototypes on every object which inherits from a prototype via concatenation:
function copyOf(object, prototype) {
var prototypes = object.prototypes;
var prototypeOf = Object.isPrototypeOf;
return prototypes.indexOf(prototype) >= 0 ||
prototypes.some(prototypeOf, prototype);
}
Hence prototypal inheritance is just as powerful as classical inheritance. In fact it's much more powerful than classical inheritance because in prototypal inheritance you can hand pick which properties to copy and which properties to omit from different prototypes.
In classical inheritance it's impossible (or at least very difficult) to choose which properties you want to inherit. They use virtual base classes and interfaces to solve the diamond problem.
In JavaScript however you'll most likely never hear of the diamond problem because you can control exactly which properties you wish to inherit and from which prototypes.
3. Prototypal Inheritance is Less Redundant
This point is a little more difficult to explain because classical inheritance doesn't necessarily lead to more redundant code. In fact inheritance, whether classical or prototypal, is used to reduce the redundancy in code.
One argument could be that most programming languages with classical inheritance are statically typed and require the user to explicitly declare types (unlike Haskell which has implicit static typing). Hence this leads to more verbose code.
Java is notorious for this behavior. I distinctly remember Bob Nystrom mentioning the following anecdote in his blog post about Pratt Parsers:
You gotta love Java's "please sign it in quadruplicate" level of bureaucracy here.
Again, I think that's only because Java sucks so much.
One valid argument is that not all languages which have classical inheritance support multiple inheritance. Again Java comes to mind. Yes Java has interfaces, but that's not sufficient. Sometimes you really need multiple inheritance.
Since prototypal inheritance allows for multiple inheritance, code which requires multiple inheritance is less redundant if written using prototypal inheritance rather than in a language which has classical inheritance but no multiple inheritance.
4. Prototypal Inheritance is Dynamic
One of the most important advantages of prototypal inheritance is that you can add new properties to prototypes after they are created. This allows you to add new methods to a prototype which will be automatically made available to all the objects which delegate to that prototype.
This is not possible in classical inheritance because once a class is created you can't modify it at runtime. This is probably the single biggest advantage of prototypal inheritance over classical inheritance, and it should have been at the top. However I like saving the best for the end.
Conclusion
Prototypal inheritance matters. It's important to educate JavaScript programmers on why to abandon the constructor pattern of prototypal inheritance in favor of the prototypal pattern of prototypal inheritance.
We need to start teaching JavaScript correctly and that means showing new programmers how to write code using the prototypal pattern instead of the constructor pattern.
Not only will it be it easier to explain prototypal inheritance using the prototypal pattern, but it will also make better programmers.
If you liked this answer then you should also read my blog post on "Why Prototypal Inheritance Matters". Trust me, you will not be disappointed.
Allow me to actually answer the question inline.
Prototype inheritance has the following virtues:
- It is better suited to dynamic languages because the inheritance is as dynamic as the environment it is in. (The applicability to JavaScript should be obvious here.) This permits you to do things quickly on the fly like customizing classes without huge amounts of infrastructure code.
- It is easier to implement a prototyping object scheme than the classic class/object dichotomy schemes.
- It eliminates the need for the complex sharp edges around the object model like "metaclasses" (I never metaclass I liked... sorry!) or "eigenvalues" or the like.
It has the following disadvantages however:
- Type checking a prototype language isn't impossible, but it's very, very difficult. Most "type checking" of prototypical languages is pure run-time "duck typing"-style checks. This is not suitable to all environments.
- It is similarly difficult to do things like optimizing method dispatch by static (or, often, even dynamic!) analysis. It can (I stress: can) be very inefficient very easily.
- Similarly object creation can be (and usually is) much slower in a prototyping language than it can be in a more conventional class/object dichotomy scheme.
I think you can read between the lines above and come up with the corresponding advantages and disadvantages of traditional class/object schemes. There are, of course, more in each area so I'll leave the rest up to other people answering.
IMO the major benefit of prototypal inheritance is its simplicity.
The prototypal nature of the language can confuse people who are classically trained, but it turns out that actually this is a really simple and powerful concept, differential inheritance.
You don't need to make classification, your code is smaller, less redundant, objects inherit from other, more general objects.
If you think prototypically you will soon notice that you don't need classes...
Prototypal inheritance will be much more popular in the near future, the ECMAScript 5th Edition specification introduced the Object.create
method, which allows you to produce a new object instance that inherits from another one in a really simple way:
var obj = Object.create(baseInstance);
This new version of the standard is being implemented by all browser vendors, and I think we will start to see more pure prototypal inheritance...
There is really not a lot to choose between the two methods. The basic idea to grasp is that when the JavaScript engine is given a property of an object to read, it first checks the instance and if that property is missing, it checks up the prototype chain. Here is an example that shows the difference between prototypal and classical:
Prototypal
var single = { status: "Single" },
princeWilliam = Object.create(single),
cliffRichard = Object.create(single);
console.log(Object.keys(princeWilliam).length); // 0
console.log(Object.keys(cliffRichard).length); // 0
// Marriage event occurs
princeWilliam.status = "Married";
console.log(Object.keys(princeWilliam).length); // 1 (New instance property)
console.log(Object.keys(cliffRichard).length); // 0 (Still refers to prototype)
Classical with instance methods (Inefficient because each instance stores it's own property)
function Single() {
this.status = "Single";
}
var princeWilliam = new Single(),
cliffRichard = new Single();
console.log(Object.keys(princeWilliam).length); // 1
console.log(Object.keys(cliffRichard).length); // 1
Efficient classical
function Single() {
}
Single.prototype.status = "Single";
var princeWilliam = new Single(),
cliffRichard = new Single();
princeWilliam.status = "Married";
console.log(Object.keys(princeWilliam).length); // 1
console.log(Object.keys(cliffRichard).length); // 0
console.log(cliffRichard.status); // "Single"
As you can see, since it is possible to manipulate the prototype of "classes" declared in the classical style, there is really no benefit to using prototypal inheritance. It is a subset of the classical method.
Web Development: Prototypal Inheritance vs. Classical Inheritance
http://chamnapchhorn.blogspot.com/2009/05/prototypal-inheritance-vs-classical.html
Classical Vs prototypal inheritance - Stack Overflow
Classical Vs prototypal inheritance
精彩评论