DCL

An elegant OOP with mixins + AOP for JavaScript.

dcl.superCall()

Version 1.x

dcl.superCall() is a light-weight way to call a method with the same name from the base “class”, if any. Essentially it is a way to call a method that was overridden by the current method. It is used as a method decorator.

It is defined as a property on dcl returned by mini.js and dcl.js.

Description

When you need to do a supercall, you decorate/wrap a method with dcl.superCall() using a double function pattern:

Double function pattern
1
2
3
4
5
   method: dcl.superCall(function(sup){
      return function(x){
          ...
      };
  })

The outer function always have one argument: sup (any name is fine). It is used to inject a method of super class in the inner function, which does the useful job. The outer function always returns the inner function.

The inner function takes as many arguments as required and returns the actual value. It is the workhorse of the tandem. In doing its work it can optionally call its super method.

It is worth noting that sup is an unadorned super method. Most probably you want to call it in context of a current object. Do not forget to use standard apply() or call() methods to supply an object, and/or arguments:

Calling a super
1
2
3
4
5
6
7
8
9
10
11
   calcPrice: dcl.superCall(function(sup){
      // let's inflate price by 200%
      return function(x){
          // asking for a real price in three different yet equivalent ways:
          var realPrice1 = sup.apply(this, arguments);
          var realPrice2 = sup.apply(this, [x]);
          var realPrice3 = sup.call(this, x);
          // now let's return it tripled
          return realPrice1 + realPrice2 + realPrice3;
      };
  })

It is possible that there is no super method to call (e.g., this “class” is the first one in line). In this case the injected sup will be falsy. It is a good idea to check sup for presence.

Handling possibly missing super
1
2
3
4
5
6
7
8
9
10
   log: dcl.superCall(function(sup){
      return function(msg){
          var newMsg = "LOG: " + msg;
          if(sup){
              sup.call(this, newMsg);
          }else{
              console.log(newMsg);
          }
      };
  })

The reason to use the double function pattern is described in Supercalls in JS.

Transitioning from a regular method to a method, which can execute a supercall is very simple:

Before applying a decorator
1
2
3
   method: function X(a, b, c){
      return a * b * c;
  }

In the example above we transition a method called method, which is implemented by a function called X. Let’s transition it to a supercalling method:

After applying a decorator
1
2
3
4
5
   method: dcl.superCall(function(sup){
      return function X(a, b, c){
          return a * b * c;
      };
  })

As you can see our X function is completely preserved — all arguments are the same, its code, and its return value is completely intact. We just added a decorator and a trivial wrapper function that returns X. Now our method can take advantage of a supercall, which is injected using sup argument of the wrapper.

Example

More complete example:

Example
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
var dcl = require("dcl");
// or: var dcl = require("dcl/mini");

var A = dcl(null, {
  method: function(x, y){
      // we will call this method implicitly from B
      console.log("A: ", x, y);
      return x + y;
  }
});

var B = dcl(A, {
  method: dcl.superCall(function(sup){
      return function(x, y){
          var r = 0;
          console.log("B: ", x, y);
          if(sup){
              r = sup.call(this, x, y);
              // or: r = sup.apply(this, arguments);
              // or: r = sup.apply(this, [x, y]);
          }else{
              console.log("B: no super");
          }
          return r + 1;
      };
  })
});

In the example above B can always expect that A comes before it, and method() in B always have a super method, meaning that checking for sup to be truthy/falsy was unnecessary. But even in trivial cases like in the example above possible modifications/refactoring may subtly change your assumptions making the check necessary.

Notes

  1. If a method is present in an Object, it will be the last in line of potential super calls.
  2. It is not necessary to call a super method. This decision can be made dynamically.
  3. The funky double function pattern allows for static chaining of super calls. In this context “static” means “once at constructor definition”.
    1. It makes super calls as cheap as possible. No extra expences per call.
    2. It makes debugging simple: going inside a super call brings a programmer directly to the next method without any stubs, thunks, or wrapper functions.
    3. Even if a programmer failed to check if sup is truthy and called it anyway, JavaScript will generate an exception pointing directly to the site of failure without any intermediaries.
    4. Both functions should be unique and created dynamically exactly like in examples.
  4. If a super method throws an exception, it is a programmer’s responsibility to catch it, to ignore it, or to pass it through.

FAQ

Is it possible to call built-in functions like that?

Yes.

Calling toString()
1
2
3
4
5
6
7
8
var A = dcl(null, {
  toString: dcl.superCall(function(sup){
      return function(){
          // no need to check if sup exists here
          return "prefix-" + sup.call(this) + "-postfix";
      };
  })
});

Is it possible to use a super call in a chained method?

Yes. In this case, if a super is not called, a chain is interrupted. It is one of the methods to interrupt chained constructors. Think twice before interrupting construction chains — usually it is a bad idea.

Interrupting constructors
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
var A = dcl(null, {
  constructor: function(){
      console.log("A");
  }
});

var B = dcl(A, {
  constructor: function(){
      console.log("B");
  }
});

var C = dcl(A, {
  constructor: dcl.superCall(function(sup){
      // we don't use sup at all
      return function(){
          console.log("C");
      };
  })
});

var b = new B();
// A
// B

var c = new C();
// C

The same can be done with any chained method. See chainBefore() and chainAfter() for more details.