-1
\$\begingroup\$

If I have this basic structure

var Foo = function(str) 
{
    console.log('Object string: ' + this.str);
    console.log('Param string : ' + str);
}

var Bar = function()
{
    this.str = 'object string';
}

var obj = new Bar();

Foo.call(obj, 'param string');

It's fairly obvious what .call() is doing in context. I recently came across some articles that went into depth on abstracting the .call() function. The premise is along these lines (this code is verging on pseudo code as I have stripped it down for ease of reading) :

var Extension = {
    // Fire a callback in global scope
    fireCallback : function(event, args) {
        this.callbacks[event].apply(window, args);
    },

    // Attach a callback
    addCallback : function(event, fn) {
        this.callbacks[event].push(fn);
    },

    // Now the bit that I really can't wrap my head around
    enable : function() {
        var self = this;

        if ( ! self.callbacks ) {
            self.callbacks = {};
        }

        self.fireCallback = function(event, args) {
            Extension.fireCallback.call(self, event, args);
        };

        self.addCallback = function(event, fn) {
            Extension.addCallback.call(self, event, fn);
        };
    }
}

There is some form of JS voodoo going on here that I can't programatically work out, can anyone help me shed light on how JS is interpreting this and why when I call:

Extension.enable.call(this);

within a class scope, the Extension object seems to extend the calling objects prototypes allowing me to bind a callback within the class scope. And then attach a handler after I have instantiated the class.

function Bar() {
    Extension.enable.call(this);

    // Do stuff

    this.fnCallback = function()
    {
        this.fireEvent('complete');
    }
}

var bar = new Bar();

bar.addCallback('complete', function(args)
{
    console.log('complete');
});

bar.fnCallback(); // logs 'complete' to console

The logic is really confusing me, I understand how call() and apply() work in the simple example above but I have no idea how or why the prototypes of the object are extended when I call Extension.enable.call(this);.

Could anyone help me shed some light on this?

\$\endgroup\$

2 Answers 2

1
\$\begingroup\$

Flambino has it mostly right, my explanation may help or hinder. Anyhow, here goes.

> var Foo = function(str)

I don't know why you are using a function expression, a declaration seems more suitable:

function Foo(str) {

.

var obj = new Bar();

Foo.call(obj, 'param string');

It's fairly obvious what .call() is doing in context

Perhaps, but unless you say what you think is going on, we can't know if your understanding is correct. And while the mechanics are fairly straight forward, it is more important to know what you are trying to do.

Such explanations are important to confirm that you and others are thinking the same thing.

... can anyone help me shed light on how JS is interpreting this and why when I call:

Extension.enable.call(this);

within a class scope,

I would say "from within the constructor", as javascript doesn't have classes and only function's have scope (and constructors called with new can only create plain objects).

Extension object seems to extend the calling objects prototypes

Not the prototype but the object itself, see below.

... allowing me to bind a callback within the class scope. And then attach a handler after I have instantiated the class.

function Bar() { Extension.enable.call(this);

The enable function is called as a plain function with its this set to the new instance of Bar. So enable is adding properties ("extending") the instance itself, not its [[Prototype]] (which is Bar.prorotype).

>     this.fnCallback = function() {
>         this.fireEvent('complete');
>     }
> }

That also extends the instance (i.e. there is now bar.fnCallback property). And I think fireEvent should be fireCallback.

> var bar = new Bar();
>
> bar.addCallback('complete', function(args)  {
>     console.log('complete');
> });

That is calling the addCallback method of bar, so this within the function is bar. It rather circuitously adds a member to bar.callbacks with a name of complete and value of the provided function, effectively:

bar.callbacks['complete'] = function(args){...}; 

>
> bar.fnCallback(); // logs 'complete' to console

So that calls fnCallback with bar as this. That function calls bar.fireEvent, passing it the parameter 'complete' and setting its this to bar.

I've assumed fireEvent should be fireCallback (since fireEvent isn't defined).

Anyhow, it calls

Extension.fireCallback.call(self, event, args);

where self is bar, which ultimately calls (replacing this with bar):

bar.callbacks['complete'].apply(window, args);

So the prototype isn't invovled at all (unless I'm wrong about fireEvent). Setting this to window within the callback doesn't do anything in this case since it isn't used.

Within the function, console will be resolved on the scope chain, probably being found on the global object (window.console in a browser), then it's log property will be called, writing 'complete' to the console.

Does that help?

\$\endgroup\$
1
  • \$\begingroup\$ Thanks for the answer rob, to clear it up, I do understand how call and apply work, the question was more aimed at how the object could be bound to another objects context with a call to it's own method from within that context. Very informative answer mind you and did help me considerably. Thanks \$\endgroup\$ Dec 13, 2012 at 18:17
1
\$\begingroup\$

I may be misunderstanding your question, but I'll have a go at answering. If I'm completely off, let me know.

Technically, no prototypes are being extended, only the instance is being extended. In your last example, the Bar constructor (aka "class") does not have any of the callback functions in its prototype. However, since Bar invokes enable when being instantiated, any instance of Bar will always be extended at runtime.

If you have code like

var Extension = {
  enable: function () {
    // Here, "this" === Extension, unless overridden by call/apply
    this.sayHi = function () {
      // Here, "this" === (whatever "this" is above)
      console.log(this.text);
    };
  }
};

function Foo() {
  // Here, when instantiating, "this" will be a new instance of Foo.
  Extension.enable.call(this);
  this.text = "Hello, world";
}

var instance = new Foo;
instance.sayHi(); // logs "Hello, world"

it's equivalent to

function Foo() {
  // Here, "this" === instance of Foo
  this.sayHi = function () {
    // Here, "this" === (whatever "this" is above)
    console.log(this.text);
  };

  this.text = "Hello, world";
}

var instance = new Foo;
instance.sayHi(); // logs "Hello, world"

Said another way:
You have function F (enable in this case) in which this would normally refer to the object of which F is a property (Extension in this case).
Then you have constructor, C. When you instantiate C, the constructor's this refers to a new instance of C. Within that constructor - by using call()- F is invoked as though F is a property of that same C-instance.
So when F is invoked, its this refers to the instance of C. Thus, whatever F does to this, will be done to the C instance.

In your case, enable adds a few methods to this, so in effect it's adding those methods to the instance of Bar. In other words, if you start at the call to enable in the Bar construtor, just read through all the code in and every time you see this think of it as bar.

But note again, that all this is taking place on instances; not prototypes. It's just that since you can only get an instance by invoking the constructor, and the constructor immediately extends the instance, the extra methods seem as though they're prototypal.

I hope that made even the slightest sense - it really is pretty hard to explain.

\$\endgroup\$
3
  • \$\begingroup\$ You explanation seems to misconstrue scope. There is no scope issue here, only a misundertanding of objects and properties. \$\endgroup\$
    – RobG
    Nov 8, 2012 at 0:13
  • \$\begingroup\$ @RobG Yeah, you're right - I'll revise my answer. \$\endgroup\$
    – Flambino
    Nov 8, 2012 at 7:16
  • \$\begingroup\$ Apologies for the very late follow up to your answer! This did go someway to helping em understand what was going on, it is much clearer how this all fits together now thanks to this and RobG's answer. \$\endgroup\$ Dec 13, 2012 at 18:16

Not the answer you're looking for? Browse other questions tagged or ask your own question.