There's really two things to understand:
- prototypal inheritance has nothing to do with performance at all. The performance issues come from runtime changes to the inheritance structure.
- prototypal inheritance (and flexible object structures) are not so much inherently slower, as they are harder to optimize.
To illustrate the first claim, Ruby would be a flagship example of class based objects being abysmally slow. The problem generally persists throughout Smalltalk-like langauges, e.g. Objective-C, that employ message passing for method calls. The Objective-C runtime uses some nifty method caching to tackle the issue, but it did take them a while to get that far.
What makes method calls cheap is that the compiler (or JIT or runtime even) has some definite knowledge about the structure of an object. Be that explicitly given in terms of language features, or implicitly inferred from static analysis, the knowledge exists and the compiler can use that to optimize.
Now when the structure can change at runtime, this makes things a little tricky, because you need good heuristics to detect which portions of the code are worth being optimized at all (you want a good ratio between the frequency of the code being run to the frequency at which it changes and the cost of optimizing it), to get the best overall runtime characteristics.
So what is the point of classes in dynamic languages? Well, there must be one, because there's numerous JavaScript class systems (like that of Ext). Sure, familiarity for programmers used to classes is one reason. But the real benefit comes from helping to ensure explicit definitions of object types. Such a class definition is one (albeit complex) statement. With vanilla JavaScript constructors, you have a whole bunch of statements, that are grouped together, if you're lucky. The class structure is really just a side effect of imperative code, if you will. Class declarations are unsurprisingly meant to facilitate a more declarative style.
It's worth to note that the Self language that really made prototypal inheritance explicit (although it's really straight forward to achieve in a language with message passing), was created for an environment where you programmed a fully interactive system while it was running. You could actually see an object on screen. This allowed for a clean declaration that was still modifiable at runtime, because the declaration and the result were so intimately coupled. Without such a coupling, fiddling with object structure at runtime can quickly become an unintelligible mess that is really hard to fit in your brain, let alone reason about.
You can pretty much get prototypal inheritance if you have good support for delegation. You just delegate all unimplemented calls to an object that you consider a prototype and you're done. It's more flexible. However it's equally harder to optimize.
What you want to do is possible and there are available in many frameworks that can help here. Look at AJAX as a callback implemented in JS and not something different - this will help simplify your problem. There is no best practice IMO, but there are some options available that I have used in the past. Here are some options,
JSP Tag UI Components: You have some widget which requires some business input to render but is mostly JavaScript. While AJAX is an option it can be unnecessary if the data is available at the point of rendering the JSP. One approach is to create a JSP Tag Lib that internally renders the HTML and JS. The benefit is fewer people are touching and working with JS which is more error prone and relatively difficult to Unit Test. In the past this was a messy option since you would have to use multiple out.println statements to render the Dynamic JS from the Tag Handler but now with the introduction of Tag Files this can be a lot cleaner.
JS Endpoint: JS Endpoints are special servlets that serve dynamic JS. Think of this instead you setting a static file in the src attribute of the script tag you have the URL pointing to a URL pointing to a Servlet that responds with content type for JavaScript or JSON as the case may be.
<script src="/myapp/abc.jsx"></script>
In the web.xml,
<servlet-mapping>
<servlet-name>myServletThatServesDynamicJS</servlet-name>
<url-pattern>*.jsx</url-pattern>
</servlet-mapping>
Client-side Frameworks: There are many JS frameworks worth your time investment that can minimize the amount of JS you write by declaratively generate a lot of JS (and CSS) something that most Java developers would prefer. AngularJS and Bootstrap are examples. So you basically add some additional attributes to regular HTML tags and there is some magic in the background that generates components, makes AJAX calls, etc. Angular will pull required JS "modules" from the server as an when needed via AJAX calls and also supports advance features like DI which can make testing your JS code a breeze and also a lot cleaner.
I personally prefer using JS frameworks to solve JS problems. So I would be in favor of option 3 for most of the issues. JS frameworks have come a long way and have wide industry support especially when you are talking of Frameworks like AngularJS or even jQuery. They also address many other concerns some that you haven't raised like cross browser compatibility which can be a huge pain even if you are supporting just one browser across versions.
Best Answer
The problem is the global scope. In other words, every change you make affects the whole code base, and yours may in turn be affected in any location of the code base.
Imagine that you want to loop through all elements of an array; imagine that browsers don't support it yet. For that, you create a method
forEach
which is called this way:You implemented this method, thought about some edge cases (what about an empty array, for example?) and everything is fine, except a slight performance bug you missed: in some cases, the enumeration is very slow.
Now, somewhere else, your colleague had this great idea of having a
forEach
method, but doesn't know you have already implemented one; actually, he even tried to callforEach
on an array to ensure the method doesn't exist yet, but since your code is not executed on every page,[].forEach
returnsundefined
.So he creates his own implementation, but forgets to test it for an empty array, and, in fact, when the array is empty, his code fails.
Back to your code, one day you find a bug report telling that there is a error around
forEach
: when the array is empty, it fails. You don't know that your colleague had his own implementation offorEach
which overwrites yours on some pages. Therefore, you think that it's your code which is concerned, so you spend hours wondering why is it failing, while your unit tests pass.You end up finding the culprit, that is the duplicated
forEach
, and you decide to remove the code of your colleague—yours is better. The next day, a new bug report tells that[].forEach
returnsundefined
on some pages—that is pages where your code is not executed. WTF!Together with your colleague, you spend additional two hours resolving this issue. Finally, you are now sure that your code is executed on every page, and you have a clean code where the prototype is added only if there is no
forEach
method already:Meanwhile, the small performance bug you had in your code is discovered, but it appears that it is more a feature, and several of your colleagues are relying on this bug: in several locations, they absolutely need the enumeration to be slow in order to have enough time to show a few images. Since rewriting their code seems to be a nightmare, you decide to keep the bug.
A year later, one morning, you discover dozens of bug reports from dozens of customers. Something is going on. Somebody on your team discovers that all bug reports are from users of Chrome, and further analysis finds that the JavaScript engine of the newly released version of Chrome has
forEach
, with the difference that it doesn't have your bug/feature your colleagues were relying on. Now, your team will spend two days adapting the code to the new, bug-less implementation offorEach
.