8

I am building a middleware layer between my Socket.IO events and the rest of my application. I do this so that I can swap out Socket.IO for something else in the future.

I store callback functions in an array. When a specific event triggers, I loop over the array and execute the callback functions. That works like a charm.

The problem lies in the removing of a callback from that array. When a callback function needs to be removed, I loop over the array and check every array item to see if it is equal (using ===) to the callback I want to remove. When just the callback is stored in the array, this works fine. However, when a callback is stored in combination with .bind(), the equal check returns false.

I created a (simplified) codepen to demonstrate the problem: http://codepen.io/petergoes/pen/wWPJdg?editors=0012.

I draw inspiration from the Socket.IO code to write my removal method: https://github.com/socketio/socket.io-client/blob/master/socket.io.js#L1623. However, there the same problem occurs.

The big question:
How can I compare two functions when (one or both of them) are called with the .bind() method?

I found this answer how do I compare 2 functions in javascript. However, comparing the string version of a function feels a bit sketchy

The codepen for reference:

var callbackList = [];

var objA = {
    myCallbackFunction: function myCallbackFunction() {
        console.log('hello my name is:', this.myName);
    }
}

var objB = {
    register: function register(callback) {
        console.log('register callback');
        callbackList.push(callback);
    },

    remove: function remove(callback) {
        console.log('remove callback');
        if(callbackList[0] === callback) {
            console.log('callback found, splice it');
            callbackList.splice(0, 1);
        } else {
            console.log('callback NOT found!');
        }
        console.log('callbackList length:', callbackList.length);
    }
}

objB.register(objA.myCallbackFunction.bind({myName: 'Peter'}));
objB.remove(objA.myCallbackFunction.bind({myName: 'Peter'}));

console.log('\nreset callbackList\n');
callbackList = [];

objB.register(objA.myCallbackFunction);
objB.remove(objA.myCallbackFunction);
Peter Goes
  • 459
  • 4
  • 12
  • I guess this doesn't work with `bind` (except with any hack probably). You shouldn't rely on function identities or function names anyway. This kind of meta programming leads to bad code (in my opinion). –  Jul 12 '16 at 15:13
  • @LUH3417 What do you mean by function identities? – Peter Goes Jul 12 '16 at 15:18
  • Given is `const f = x => x + 1, g = x => x + 1` than the following expression compares the identity of the function objects `f === g`, which yields `false`. Hence they are equivalent, but not identical. –  Jul 12 '16 at 15:23

3 Answers3

1

I think the best solution here is to store a key alongside your callback inside of the array. This is a lot more reliable and way less hacky than any solution comparing functions:

register: function register(callback, key) {
        console.log('register callback');
        var obj = {
           key: key,
           callback: callback
        }
        callbackList.push(obj);
    }

you can then call remove by passing the key desired rather than the callback, and you can compare as such:

if(callbackList[0].key === key)

Just make sure you pass the desired key when registering and access the callback property within the array objects where necessary.

I think this solution using labels is much cleaner and doesn't require any confusing or strange code.

Pabs123
  • 3,385
  • 13
  • 29
  • shouldn't it be `callbackList.push(obj);`?? – charlietfl Jul 12 '16 at 15:33
  • @Pabs123 I don't quite agree that an extra key is less confusing. Looking at the Socket.IO or jQuery api for registering an event (`socket.on('eventname', myFunction)`, `$('.myElement').on('eventname', myFunction)`) is (to me) cleaner than `myEvents.register('eventName', myFunction, 'myKey')`. Or do I miss your point? (I know my initial example lacks the `'eventName'` argument, I omitted it for the sake of clarity of the example) – Peter Goes Jul 12 '16 at 15:45
  • the point i'm trying to make is that messing around with function property comparisons or straight function equalities isn't a very good practice. there's lots of strange behaviours which you can encounter down the road and in general I believe that comparing complex objects straight up is not the best approach to this kind of simple conditional. Think of a real world example, you wouldn't walk down your street checking which house is yours by comparing every single item and its position within the house to a list that you have printed out, but rather use the house number to identify it. – Pabs123 Jul 12 '16 at 15:50
1

Consider this:

function a(x) { return x; }

var b = a.bind({ foo: 'foo' });

a !== b is true because a.bind returns a new function, that is a new object, and object are compared by reference.

The hack:

Keep in mind messing up with native prototypes is not recomended, this is a nasty hack and using keys as @Pabs123 said might be a better approach

Function.prototype.bind = (function () {
    var originalBind = Function.prototype.bind;
    return function (obj) {
        var bound = originalBind.apply(this, Array.prototype.slice.call(arguments, 1));
        bound.original = this;

        return bound;
    };
}());

Now have a reference to the function that has been bound and you can check that one too when comparing:

a = function (x) { return x; };
b = a.bind({});
b.original === a; // true

Note that the hack must be implemented before any binding is done, any calls to bind before the hack executes will not produce the original property on the result function.


A simmilar approach, but without doing the hack would be to do your custom bind method that keeps the reference to the original function. But you'll need to use this method instead of the native bind in order to get the reference to the original function.

Marco Scabbiolo
  • 7,203
  • 3
  • 38
  • 46
0

I found storing a reference to the bound function seems to work:

    var callbackList = [];

    var objA = {
        myCallbackFunction: function myCallbackFunction() {
            console.log('hello my name is:', this.myName);
        }
    }

    var objB = {
        register: function register(callback) {
            console.log('register callback');
            callbackList.push(callback);
        },

        remove: function remove(callback) {
            console.log('callbackList.length before:', callbackList.length)
            var cleanList = [];
            callbackList.forEach(function(cb){
                if( cb !== callback ){
                    cleanList.push(cb);
                }
            })
            callbackList = cleanList;
            console.log('callbackList.length after:', callbackList.length)
        }
    }

    var f = objA.myCallbackFunction.bind({myName: 'Peter'});
    var f2 = objA.myCallbackFunction.bind({myName: 'Peter'});
    objB.register(f);
    objB.register(f2);
    objB.remove(f);

I think the reason you need a reference is because bind returns a new function. So, comparison doesn't work in your example because they are actually two different functions.

dan_paul
  • 309
  • 1
  • 10
  • Not sure why this is down voted. If it's acceptable to add additional data to track the bound function (as in the accepted answer), why not just use a reference to the bound function? Both solutions require tracking the the bound function but the key solution requires associating additional unnecessary data with it. Additionally, this solution actually answers the original question. You compare bound functions by comparing their references. – dan_paul Jul 12 '16 at 20:10