javascriptfunctionbindingdelegationarity

How to dynamically create a method with a variable number of parameters stored in a variable?


In order to set up a Socket.IO client, I have a bunch of methods looking like this:

myobject.prototype.A = function (callback) {
    this.foo('a', null, callback);
}

myobject.prototype.B = function (bar, callback) {
    this.foo('b', { bar }, callback);
}

myobject.prototype.C = function (baz, qux, callback) {
    this.foo('c', { baz, qux }, callback);
}

The content of this.foo is unimportant, but it takes 3 parameters: a string, an object built from the calling method parameters, and a callback method.

I'd like to have the methods set up in a single place. I'd like to have something looking like this:

// I'm not sure what form the args should take
const methods = {
  A: { socketName: 'a', args: [ ] },
  B: { socketName: 'b', args: [ 'bar' ] },
  C: { socketName: 'c', args: [ 'baz', 'qux' ] }
};

for (let m in methods) {
    const mData = methods[m];
    this.prototype[m] = function (what_do_I_put_here_?, callback) {
        // how do I form "otherArgs" ?
        this.foo(mData.socketName, otherArgs, callback);
    }
}

I think I'll have to look to destructuring assignments but I'm not sure how to use them in this case.


Solution

  • You could do that utilizing closures and an array function:

    "use strict"
    class Test {
    
      createDynFoo(name, propertyNames = []) {
        // return an arrow function (ensures that this is still the obj)
        return (x, ...args) => {
          let obj = null; // set obj to null as default
          if (args.length > 0) {
            // if we have additional aguments to x we create an obj
            obj = {};
            // map the additional arguments to the property names
            propertyNames.forEach((value, idx) => {
              obj[value] = args[idx]
            })
          }
          // call the actual foo function
          return this.foo(name, x, obj)
        }
      }
    
      foo(name, x, obj) {
        console.log(`${name} ${x}`)
        console.dir(obj);
      }
    }
    
    let test = new Test();
    
    
    let tiggerForA = test.createDynFoo('a');
    let tiggerForB = test.createDynFoo('b', ['y']);
    let tiggerForC = test.createDynFoo('c', ['y', 'z']);
    
    tiggerForA(1);
    tiggerForB(1, 2);
    tiggerForC(1, 2, 3);

    If you really need it as member functions you could do:

    "use strict"
    class Test {
    
      constructor() {
        this.A = this.createDynFoo('a');
        this.B = this.createDynFoo('b', ['y']);
        this.C = this.createDynFoo('c', ['y', 'z']);
      }
    
    
      createDynFoo(name, propertyNames = []) {
        // return an arrow function (ensures that this is still the obj)
        return (x, ...args) => {
          let obj = null; // set obj to null as default
          if (args.length > 0) {
            // if we have additional aguments to x we create an obj
            obj = {};
            // map the additional arguments to the property names
            propertyNames.forEach((value, idx) => {
              obj[value] = args[idx]
            })
          }
          // call the actual foo function
          return this.foo(name, x, obj)
        }
      }
    
      foo(name, x, obj) {
        console.log(`${name} ${x}`)
        console.dir(obj);
      }
    }
    
    let test = new Test();
    
    
    test.A(1);
    test.B(1, 2);
    test.C(1, 2, 3);

    If it is only about member functions you could get rid of the the arrow function like this:

    "use strict"
    
    
    
    function createDynFunction(classObj, targetFunctionName, newFunctionName, name, propertyNames = []) {
      // create a new function and assigned it to the prototype of the class
      classObj.prototype[newFunctionName] = function(x, ...args) {
        let obj = null; // set obj to null as default
        if (args.length > 0) {
          // if we have additional aguments to x we create an obj
          obj = {};
          // map the additional arguments to the property names
          propertyNames.forEach((value, idx) => {
            obj[value] = args[idx]
          })
        }
        // call the actual foo function
        return this[targetFunctionName](name, x, obj)
      }
    }
    
    class Test {
      foo(name, x, obj) {
        console.log(`${name} ${x}`)
        console.dir(obj);
      }
    }
    
    createDynFunction(Test, 'foo', 'A', 'a');
    createDynFunction(Test, 'foo', 'B', 'b', ['y']);
    createDynFunction(Test, 'foo', 'C', 'c', ['y', 'z']);
    
    let test = new Test();
    
    test.A(1);
    test.B(1, 2);
    test.C(1, 2, 3);

    As I don't know the exact use case, it is hard to come up with a solution that exactly matches the needs. But based on the shown code, you should get an idea how this can be achieved.