Matt Huggins
Matt Huggins

Reputation: 83279

Is it possible to call function.apply without changing the context?

In some Javascript code (node.js specifically), I need to call a function with an unknown set of arguments without changing the context. For example:

function fn() {
    var args = Array.prototype.slice.call(arguments);
    otherFn.apply(this, args);
}

The problem in the above is that when I call apply, I'm change the context by passing this as the first argument. I'd like to pass args to the function being called without changing the context of the function being called. I essentially want to do this:

function fn() {
    var args = Array.prototype.slice.call(arguments);
    otherFn.apply(<otherFn's original context>, args);
}

Edit: Adding more detail regarding my specific question. I am creating a Client class that contains a socket (socket.io) object among other info pertaining to a connection. I am exposing the socket's event listeners via the client object itself.

class Client
  constructor: (socket) ->
    @socket    = socket
    @avatar    = socket.handshake.avatar
    @listeners = {}

  addListener: (name, handler) ->
    @listeners[name] ||= {}
    @listeners[name][handler.clientListenerId] = wrapper = =>
      # append client object as the first argument before passing to handler
      args = Array.prototype.slice.call(arguments)
      args.unshift(this)
      handler.apply(this, args)  # <---- HANDLER'S CONTEXT IS CHANGING HERE :(

    @socket.addListener(name, wrapper)

  removeListener: (name, handler) ->
    try
      obj = @listeners[name]
      @socket.removeListener(obj[handler.clientListenerId])
      delete obj[handler.clientListenerId]

Note that clientListenerId is a custom unique identifier property that is essentially the same as the answer found here.

Upvotes: 37

Views: 14252

Answers (10)

YakovL
YakovL

Reputation: 8317

These days you can use rest parameters:

function fn(...args) {
    otherFn(...args);
}

The only downside is, if you want to use some specific params in fn, you have to extract it from args:

function fn(...args) {
    let importantParam = args[2]; //third param
    // ...
    otherFn(...args);
}

Here's an example to try (ES next version to keep it short):

// a one-line "sum any number of arguments" function
const sum = (...args) => args.reduce((sum, value) => sum + value);

// a "proxy" function to test:
var pass = (...args) => sum(...args);
console.log(pass(1, 2, 15));

Upvotes: 1

Blackfire
Blackfire

Reputation: 73

If you bind the function to an object and you use everywhere the bound function, you can call apply with null, but still get the correct context

var Person = function(name){
    this.name = name;
}
Person.prototype.printName = function(){
    console.log("Name: " + this.name);
}

var bob = new Person("Bob");

bob.printName.apply(null); //window.name
bob.printName.bind(bob).apply(null); //"Bob"

Upvotes: 5

Matt Huggins
Matt Huggins

Reputation: 83279

I was just reminded of this question after a long time. Looking back now, I think what I was really trying to accomplish here was something similar to how the React library works with its automatic binding.

Essentially, each function is a wrapped bound function being called:

function SomeClass() {
};

SomeClass.prototype.whoami = function () {
  return this;
};

SomeClass.createInstance = function () {
  var obj = new SomeClass();

  for (var fn in obj) {
    if (typeof obj[fn] == 'function') {
      var original = obj[fn];

      obj[fn] = function () {
        return original.apply(obj, arguments);
      };
    }
  }

  return obj;
};

var instance = SomeClass.createInstance();
instance.whoami() == instance;            // true
instance.whoami.apply(null) == instance;  // true

Upvotes: 0

Dave
Dave

Reputation: 2134

Simply put, just assign the this to what you want it to be, which is otherFn:

function fn() {
    var args = Array.prototype.slice.call(arguments);
    otherFn.apply(otherFn, args);
}

Upvotes: 7

glenatron
glenatron

Reputation: 11362

One way that you can work around the change of context that can happen in JavaScript when functions are called, is to use methods that are part of the object's constructor if you need them to be able to operate in a context where this is not going to mean the parent object, by effectively creating a local private variable to store the original this identifier.

I concede that - like most discussions of scope in JavaScript - this is not entirely clear, so here is an example of how I have done this:

function CounterType()
{
    var counter=1;
    var self=this; // 'self' will now be visible to all

    var incrementCount = function()
    {
        // it doesn't matter that 'this' has changed because 'self' now points to CounterType()
        self.counter++;
    };

}

function SecondaryType()
{
    var myCounter = new CounterType();
    console.log("First Counter : "+myCounter.counter); // 0
    myCounter.incrementCount.apply(this); 
    console.log("Second Counter: "+myCounter.counter); // 1
}

Upvotes: 1

Brian McCutchon
Brian McCutchon

Reputation: 8584

If I understand you correctly:

                          changes context
                   |    n     |      y       |
accepts array    n |  func()  | func.call()  |
of arguments     y | ???????? | func.apply() |

PHP has a function for this, call_user_func_array. Unfortunately, JavaScript is lacking in this regard. It looks like you simulate this behavior using eval().

Function.prototype.invoke = function(args) {
    var i, code = 'this(';
    for (i=0; i<args.length; i++) {
        if (i) { code += ',' }
        code += 'args[' + i + ']';
    }
    eval(code + ');');
}

Yes, I know. Nobody likes eval(). It's slow and dangerous. However, in this situation you probably don't have to worry about cross-site scripting, at least, as all variables are contained within the function. Really, it's too bad that JavaScript doesn't have a native function for this, but I suppose that it's for situations like this that we have eval.

Proof that it works:

function showArgs() {
    for (x in arguments) {console.log(arguments[x]);}
}

showArgs.invoke(['foo',/bar/g]);
showArgs.invoke([window,[1,2,3]]);

Firefox console output:

--
[12:31:05.778] "foo"
[12:31:05.778] [object RegExp]
[12:31:05.778] [object Window]
[12:31:05.778] [object Array]

Upvotes: 9

Blake Regalia
Blake Regalia

Reputation: 2766

Since you seem to want to be using the bind function as it is defined in Javascript 1.8.5, and be able to retrieve the original this object you pass the bind function, I recommend redefining the Function.prototype.bind function:

Function.prototype.bind = function (oThis) {
    if (typeof this !== "function") {
        throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");
    }

    var aArgs = Array.prototype.slice.call(arguments, 1),
        fToBind = this,
        fNOP = function () {},
        fBound = function () {
            return fToBind.apply(this instanceof fNOP && oThis
            ? this
            : oThis,
            aArgs.concat(Array.prototype.slice.call(arguments)));
        };

    fNOP.prototype = this.prototype;
    fBound.prototype = new fNOP();

    /** here's the additional code **/
    fBound.getContext = function() {
        return oThis;
    };
    /**/

    return fBound;
};

Now you can retrieve the original context that you called the bind function with:

function A() {
    return this.foo+' '+this.bar;
}

var HelloWorld = A.bind({
    foo: 'hello',
    bar: 'world',
});

HelloWorld(); // returns "hello world";
HelloWorld.getContext(); // returns {foo:"hello", bar:"world"};

Upvotes: 0

Matt Huggins
Matt Huggins

Reputation: 83279

I'm not going to accept this as an answer, as I'm still hoping for something more suitable. But here's the approach I'm using right now based upon the feedback on this question so far.

For any class that will be calling Client.prototype.addListener or Client.prototype.removeListener, I did added the following code to their constructor:

class ExampleClass
  constructor: ->
    # ...
    for name, fn of this
      this[name] = fn.bind(this) if typeof(fn) == 'function'

   message: (recipient, body) ->
     # ...

   broadcast: (body) ->
     # ...

In the above example, message and broadcast will always be bound to the new ExampleClass prototype object when it's instantiated, allowing the addListener code in my original question to work.

I'm sure some of you are wondering why I didn't just do something like the following:

example = new ExampleClass
client.addListener('message', example.bind(example))
# ...
client.removeListener('message', example.bind(example))

The problem is that every time .bind( ) is called, it's a new object. So that means that the following is true:

example.bind(example) != example.bind(example)

As such, the removeListener would never work successfully, thus my binding the method once when the object is instantiated.

Upvotes: 0

Blake Regalia
Blake Regalia

Reputation: 2766

Just push properties directly to the function's object and call it with it's own "context".

function otherFn() {
    console.log(this.foo+' '+this.bar); // prints: "hello world" when called from rootFn()
}

otherFn.foo = 'hello';
otherFn.bar = 'world';

function rootFn() {
    // by the way, unless you are removing or adding elements to 'arguments',
    // just pass the arguments object directly instead of casting it to Array
    otherFn.apply(otherFn, arguments);
}

Upvotes: -1

Scott Sauyet
Scott Sauyet

Reputation: 50787

'this' is a reference to your function's context. That's really the point.

If you mean to call it in the context of a different object like this:

otherObj.otherFn(args)

then simply substitute that object in for the context:

otherObj.otherFn.apply(otherObj, args);

That should be it.

Upvotes: 6

Related Questions