Supercalls (calling a super method, which was inherited and overridden) are an essential part of any inheritance scheme. This is a facility that allows us to augment behavior of existing objects, paving a way to decompose large monolithic objects into a collection of small incremental “classes”. If used correctly, such “classes” can provide a completely orthogonal set of building blocks, which will help us to keep an overall complexity down.
While ES5 provides means to delegate methods of one object to another, there is no native provisions for supercalls. It means that it should be provided by a helper.
(ES6 introduces super calls, but with some restrictions. For example, methods, which use
super, when copied to a different object are not rebound to a new super method. Below we discuss ES5 exclusively. There is a different version of
dcl, which supports ES6 and TypeScript.)
This is as native as it can be:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
The whole thing looks like a clumsy pattern, and patterns are usually sure signs of a weakness of an underlying platform. Things like that are brittle, and any attempt to refactor something will involve a lot of editing. For example, if we are to change name of a class, it will involve hunting it down and changing it everywhere.
Frequently mixins don’t know (and don’t care) about their immediate parents. It would be impossible to name super methods explicitly inside mixin’s methods. This consideration alone makes direct calls a poor choice for anything but small programs.
An advantage of direct calls is obvious too — calling a super is practically static making it relatively fast: just 2 dictionary lookups and
call() overhead per call.
The idea is simple: let’s wrap our super-calling methods with a functional wrapper, which will set up a supercall for us.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
Obviously the above code is a simplified implementation of that idea. It can be implemented differently.
We can see an advantage immediately: this method doesn’t force us to use
call() for the sake of supplying the right object to a super method, which makes a supercall cheaper.
- It reserves a special name for a current super method. In the example above it was
- The wrapper is called once per every call to the wrapped method.
- The wrapper function doubles a number of method calls. It is a small price for big and complex methods, but for the small, yet frequently called, methods the overhead of the setup wrapper can be significant.
- We “pay” for every call.
- Having a wrapper affects debugging in a negative way: we have to step over the same statements inside a wrapper, when we are debugging super-calling methods. It becomes old very fast.
- The setup code runs even if we don’t use a supercall (e.g., bypassing it dynamically).
Nevertheless this is a very popular technique with OOP libraries. Some libraries apply it to every method when constructing objects/“classes”. This way we can call supers in any method without marking them up in any way. The downside is obvious too — all methods pay “wrapper taxes” listed above.
Another popular trick is to inspect a method’ source to deduce if it uses a supercall. It is a simple yet effective optimization, which is tolerant to false positives — if we made a mistake it will cost user a performance penalty, but doesn’t break their code. Some browsers do not keep function sources for memory conservation reasons, and this trick cannot work with them, yet such browsers are exceedingly rare nowadays.
Automatic wrapping works only when objects/“classes” were produced by a special function. If this technique is to be used with manually created objects, it should be explicit like in the example above.
Let’s spell out complexity of this method: an extra function call per a wrapped method call.
declare() is famous for supporting such style. The idea is to have a function, which we can call to figure out our super method:
1 2 3 4 5 6 7 8
Again the example above is over-simplified. Yet it highlights main properties of this extremely user-friendly technique: it doesn’t require any kind of special call to mark a method as super-calling, there is no mandatory wrapper, which simplifies debugging, no performance tax to pay if a super is not called.
- It reserves a special name. In the example above it was
this.inherited()figures out a super method dynamically.
- The algorithm is much more complex (and more expensive) than the wrapper’s one making it very expensive for small frequently called methods.
- Just like with auto-wrapping above a special “class” creator function is required to produce such metadata.
- Call to a super is effectively wrapped, which is a negative factor for debugging (more unrelated code to skip).
this.inherited()as in the example above does not work in strict mode. It needs to know its caller to figure out what to call next, and it takes a caller from
arguments.callee. This is a smart move, which simplifies life of a programmer, yet it is prohibited in strict mode.
- This can be remedied by specifying the caller (itself) explicitly. It does the job, but the elegance is lost.
The performance aspect can be relieved by an elaborate caching mechanism, yet it cannot be more performant than a wrapper.
The complexity: one extra function call with a non-trivial algorithm inside per supercall.
So far we examined direct calls, calls using an object-wide global variable set by a wrapper, calls to an object-wide global method that figures out our super dynamically, the only thing left is pulling it from a closure.
1 2 3 4 5 6 7 8 9 10
As you can see the internal function pulls a super method from the outer function. It allows for the outer function to be called during “class”/object creation time, which returns its internal function to be called as a method.
The outer function is called once per “class” creation, and returns the closure (the inner function) with its super bound. After that its never called, and all calls to super methods go directly to the inner function. This way we have no performance penalty per call.
Let’s count pros:
- No need to reserve a name like with wrappers and
- No price to pay for methods not using supercalls both statically and dynamically.
- No price to pay for supercalls.
- No wrappers whatsoever.
- Debugging is completely straight-forward.
- The pattern looks weird. Like all patterns it can be mistyped.
- All super-calling methods should be converted to it.
- There is no automation like with other techniques.
- Super-calling methods are not directly usable.
- A “class” creator function is required that should instantiate necessary super-calling methods.
All techniques have pros and cons. Usually a programmer makes a conscience decision selecting strong features and trade-offs, which are right for their application. A library writer cannot afford to make choices that penalize application developers. An example of such decision would be a performance.
Out of last three techniques the double function one is virtually without a run-time penalty per call (only a small setup “fee” per “class” is expected), makes debugging comfortable, and does not incurr any penalty if not used. Thus it was selected to be implemented as the mechanism for supercalls: dcl.superCall().
It is worth noting that dcl 1.x implements both the double function method and
this.inherited() to support legacy code.