skip to Main Content

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.

2

Answers


  1. 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.

    Login or Signup to reply.
  2. const toObj = (keys, values) =>
      keys.length === 0
        ? null
        : _.zipObject(keys, values); 
    
    function wrap(fn, name, keys) {
      return function(...args) {
        const callback = args.pop();
        const obj = toObj(keys, args);
        
        return fn.call(this, name, obj, callback);
      }
    }
    
    class Foo {
      foo(name, obj, callback) { 
        //calling a method to show that the value of `this` is forwarded in the call
        console.log(this.bar(name, obj, callback));
      }
      
      bar(name, obj, callback) {
        return `name: '${name}'; obj: '${JSON.stringify(obj)}', callback: '${callback}'`;
      }
    }
    
    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];
        Foo.prototype[m] = wrap(Foo.prototype.foo, mData.socketName, mData.args);
    }
    
    const foo = new Foo();
    
    foo.A(() => "this is A's callback");
    foo.B("hello", () => "this is B's callback");
    foo.C("hello", "world", () => "this is C's callback");
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/lodash.min.js"></script>

    This utilises a higher order function that can wrap/decorate functions and enforce the needed parameters. Function.call would also set the value of this when calling, thus if decorating a method it will use the value of this where it is called from as usual:

    /**
     * Wrap functions to ensure parameters are uniformly applied.
     *
     * @param {Function} fn - function to decorate
     * @param {string} name - first to be given to the function
     * @param {Array<string|number|Symbol>} keys - keys for the object which 
     * will become the second parameter of `fn`
     * 
     * @return {Function} function that will be called with variable number 
     * of arguments - the number of `keys` plus one callback. Forwards the 
     * value of `this` to the function.
     */
    function wrap(fn, name, keys) {
      return function(...args) {
        const callback = args.pop();
        const obj = toObj(keys, args);
        
        return fn.call(this, name, obj, callback);
      }
    }
    

    There is the toObj which should take the keys and varargs, then produce an object out of them. I use a ready made implementation for brevity – _.zipObject() from Lodash. However, it can be custom changed or entirely custom built, if needed.

    With that a call would look like

    wrap(someFn, "b", [ 'bar' ]
    

    which will produce a function that can be called like

    wrappedFn("hello", () => "some callback")
    

    which would in turn call

    someFn("b", [ 'bar' ], () => "some callback")
    
    const toObj = (keys, values) =>
      keys.length === 0
        ? null
        : _.zipObject(keys, values); 
    
    function wrap(fn, name, keys) {
      return function(...args) {
        const callback = args.pop();
        const obj = toObj(keys, args);
    
        return fn.call(this, name, obj, callback);
      }
    }
    
    function someFn(name, obj, callback) {
      console.log(`name: '${name}'; obj: '${JSON.stringify(obj)}', callback: '${callback}'`);
    }
    const wrappedFn = wrap(someFn, "b", [ 'bar' ]);
    
    wrappedFn("hello", () => "some callback");
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/lodash.min.js"></script>

    Two notable things:

    • the number of keys and values is not verified. If that is required, it needs to be handled
    • the length property of the wrapped function will report zero, as that is how the rest syntax for parameters is defined to work. This means that examining it to find the number of parameters before calling will be unreliable
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search