1

I have code in the following form in which the user may specify a callback which will be called at a later time:

var _deferred = [];

var deferred = function(callback) {
    _deferred.push(callback);
}

var doDeferred = function() {
    for(var i = 0, max = _deferred.length; i < max; i++) {
        _deferred[i].call();
    }
}

for(var i = 0; i < 5; i++) {
    deferred(function() {
        console.log("Some deferred stuff");
    });
}

doDeferred();

I would like to recognize that the callback specified to deferred() is an anonymous function resolving to the same origin, and only allow it to be added once. Aka in the bottom for loop it would throw an exception when i = 1.

Like:

var deferred = function(callback) {
        if(_deferred.indexOf(callback) !== -1) {
                throw "Already added!";
        }
        _deferred.push(callback);
}

I can think of many ways of doing this by adding a "key", but I'm wondering if I can use something along the lines of Function.caller to create a "source hash"?

Is there a solution for this out there already that I'm not seeing? I'd really prefer to accept this burden onto myself rather than push it out to the caller of deferred and have them provide some sort of unique id.

EDIT:

To clarify any confusion.

Yes each call to deferred is with a unique function object, has its own closure, etc. Therefore my indexOf will always fail. That is not a point of confusion.

The question is that these anonymous functions are declared in the same place, they are the same code, how can I determine that? I'm looking to determine declarative equality, not instance equality. I was thinking I could somehow create a hash based on caller of deferred...

CONCLUSION:

Thanks guys, it seems like there is no really elegant solution here. The toString method is not definitive (different functions with same body will test equally as strings) - and is just ugly. I'm going to put the burden on the caller.

amirpc
  • 1,638
  • 3
  • 19
  • 24
  • What's wrong with your `indexOf` method? What exactly do you mean by `resolving to the same origin`? – davin Jul 11 '11 at 19:54
  • Well, it doesn't throw the exception :) – amirpc Jul 11 '11 at 19:57
  • it doesn't throw the exception because it's a different function in memory – Robert Jul 11 '11 at 19:58
  • You must be doing something else wrong: `r = function(){}; v=[]; v.push(r); v.indexOf(r) !== -1;` is `true`. Of course, `v=[]; v.push(function(){}); v.indexOf(function(){}) !== -1` is `false` since those 2 functions are different. – davin Jul 11 '11 at 19:59
  • Yes, I know. I'm asking how to detect that the function in memory originated at the same location in code. I'm aware that my code is broken, and I know why it's broken. I'm not confused - I'm looking for a way to do what the intention of this code is, which is see that the origin (where it was created) of the callback is the same - regardless of the fact that each one is a unique function instance in memory. – amirpc Jul 11 '11 at 20:00

4 Answers4

1

As far as I know, if you have two anonymous functions with the same bodies, they will not be equivalent in terms of the "==" operator, so I don't think you can do the indexOf().

You could check if your new function has the same body as any function already in your array. To do this just convert the function to a string and check for equality:

String((function(){/*something*/})) == String((function(){/*something*/}))

Should return true;

Griffin
  • 13,184
  • 4
  • 29
  • 43
  • I'd say it's hard to define what an actual new function is. If he uses i in each function, then their results will be different even if the body is the same text. You'd need to analyze the scope object as well to truly know if they're equal. – Robert Jul 11 '11 at 20:00
  • I'm not looking for true equality, only that the callback came from the same point in code (aka declaratively equal) even thought the function instances are clearly not equal due to scope, etc. – amirpc Jul 11 '11 at 20:02
  • @Robert, yes it's much harder to decide whether two functions are equal than what I have suggested. But I assume he has some control over what these 'deferred' functions do, so it might be enough. – Griffin Jul 11 '11 at 20:03
  • @Robert: actually unless an extra closure wrapper were added the results would be the same: `i` will be `5` on each callback due to the [Closure Loop Problem](http://stackoverflow.com/q/2568966/18936). – bobince Jul 11 '11 at 20:07
  • @bobince: that's true; I assumed that's just sort of common knowledge if you're going to use it at a deferred time. – Robert Jul 11 '11 at 20:12
1
var deferred = function(callback) {
    if(_deferred.some(function(c){return c.toString() === callback.toString()})) {
            throw "Already added!";
    }
    _deferred.push(callback);
}

This will throw the error the first time a duplicate method signature is added.

Gabriel
  • 18,322
  • 2
  • 37
  • 44
1

The thing is, in the loop at the bottom, they are different functions, so in all fairness they should both be included (and honestly, there is no guarantee that the values from both functions won't be different depending on the variables present at the moment). I'm also not sure that 'unique functions only' is something which people expect, so it might cause a good deal of "debugging"

This isn't something which is required of ECMAScript, but Function.toString() will generally return its internal structure. So you probably want:

var ids = [] // separate ID takes up slightly more space, but lookup should 
             // be faster.
var deferred = function(callback) {
        var cbs = callback.toString() // or String(callback)
        if(ids.indexOf( cbs ) !== -1)
        {
                throw "Already added!";
        }
        ids.push( cbs )
        _deferred.push(callback);
}

If you're willing to use a for... in loop:

var _deferred = {}
var deferred = function(callback) {
        var cbs = callback.toString() // or String(callback)
        if( _deferred[ cbs] )
        {
                throw "Already added!";
        }
        _deferred[ cbs] = callback;
}

// caution, this executes in arbitrary order.
var doDeferred = function() {
    for(var i in _deferred) {
        _deferred[i].call();
    }
}
cwallenpoole
  • 79,954
  • 26
  • 128
  • 166
  • You need to make callback a string in your indexOf statement, or you're comparing a function to strings – Robert Jul 11 '11 at 20:03
  • @Robert ACK! Good call. Fixed – cwallenpoole Jul 11 '11 at 20:05
  • The one problem with this is that it cannot distinguish between multiple calls to the same anonymous function, and closures. e.g. if you had a loop like `for (var i=0;i<5;i++) { deferred((function(j) { return function() { console.log("iteration " + j)};}(j)) };` -- this would still be considered exactly the same as the inner (returned) function minus the closure. I don't have a better suggestion.. though it seems like if one wants to treat same-ref anonymous functions as duplicates for some purpose, one might not want to treat closures as so. – Jamie Treworgy Jul 11 '11 at 20:15
1

Though I wouldn't consider this elegant, you can compare anonymous functions by using their toString() method.

var deferred = function(callback) {
        var str = callback.toString();
        for var i = 0; i < _deferred.length; i++) {
            if (_deferred[i].toString() == str) {
                throw "Already added!";
            }
        }
        _deferred.push(callback);
}
jfriend00
  • 683,504
  • 96
  • 985
  • 979