6

I'm trying to create some kind of inheritance between objects:

var foo = (function(){

    function doFooStuff(){
        console.log(arguments.callee.name);
    }

    return {
        doFooStuff: doFooStuff
    }

})();

var bar = (function(){

    $.extend(this, foo);

    function doBarStuff(){
        console.log(arguments.callee.name);
        doFooStuff();
    }

    return {
        doBarStuff: doBarStuff,
    }

})();

bar.doBarStuff();
bar.doFooStuff(); //<-- Uncaught TypeError: 
                  //Object #<Object> has no method 'doFooStuff' 

http://jsfiddle.net/wRXTv/

Why isn't doFooStuff accessible here? Would you recommend another approach than using $.extend?

Johan
  • 35,120
  • 54
  • 178
  • 293
  • When you do `$.extend(this, foo);` What are you trying to do? `this` represents the global object (that's usually `window` in the browser). Did you mean to call the second IIFE like a constructor using `new function(){` ? – Benjamin Gruenbaum Sep 15 '13 at 20:35
  • possible duplicate of [How to implement inheritance in JS Revealing prototype pattern?](http://stackoverflow.com/questions/9248655/how-to-implement-inheritance-in-js-revealing-prototype-pattern) (though that is for the prototype inheritance pattern only) – Bergi Sep 15 '13 at 20:36
  • @BenjaminGruenbaum Oh, I thought it referred to `bar`. And no, I don't want to create a new instance, I just want "static" objects. – Johan Sep 15 '13 at 20:40
  • @Johan see @Bergi's answer for a link to a more detailed explanation on how `this` works. – Benjamin Gruenbaum Sep 15 '13 at 20:50

4 Answers4

7
$.extend(this, foo);

this is not the object which you return from the function below (in fact it cannot be since it's created after this call), but the global object - check MDN's introduction to the this keyword.

For what you want to do, there are two ways:

  • Copy all properties from foo onto your bar object after it is created:

    var bar = (function() {
        …
        return {…};
    })();
    $.extend(bar, foo);
    

    You can do that as well directly on the returned object:

        return $.extend({…}, foo);
    

    A variant of this pattern allows you to overwrite foo properties. Copy foo into an empty object, then write your bar properties to it:

        return $.extend({}, foo, {…});
    
  • Use prototypical inheritance. Create an object that inherits its properties from foo, and then write your bar properties to it:

        return $.extend(Object.create(foo), {…});
    

    Now when foo changes afterward, you still can access those new properties on bar (unless they're shadowed by own properties). Notice that Object.create might not be supported in legacy environments, but you can easily shim it.


As noted by @raina77ow, your doBarStuff function is flawed too. The doFooStuff function is not in the scope of your function, and you cannot change that. You never will be able to access the private declared functions and variables from the foo module, only those that are public - if you did need them, consider a different pattern or app design. However, doFooStuff is a property on the exported (public) foo object, from which bar inherits (regardless in which of the above demonstrated ways). Therefore, you can access it as a method of bar as well, usually by using this.doFooStuff().

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • Thanks Bergi. Could you give a short explanation on the difference between `$.extend(Object.create(foo)...` and `$.extend(foo...` ? – Johan Sep 15 '13 at 20:55
  • `$.extend` does copy the properties it sees one the second (and further) arguments to its first argument, creating a *snapshot* and leaving two distince objects with similar properties pointing to the same values. Instead, `Object.create` does [true inheritance](http://stackoverflow.com/questions/186244/what-does-it-mean-that-javascript-is-a-prototype-based-language), the resulting object does not have any properties on its own but looks them up on its prototype object *live*. – Bergi Sep 15 '13 at 21:02
  • 1
    Notice I never extended `foo`, I used it as a source argument only. Written out: `var bar = {}; $.extend(bar, foo); $.extend(bar, {…})` vs `var bar = Object.create(foo); $.extend(bar, {…});` – Bergi Sep 15 '13 at 21:05
5

You attempt to work with revealing module as if it were a constructor. Hence an attempt to extend this, wrong for many reasons. The most glaring, I suppose, is that neither the function is used as a constructor (no new) nor its context is changed. In plain words, this just points to a global object here.

But that's not the only problem. Consider that part of your code:

function doBarStuff(){
  console.log(arguments.callee.name);
  doFooStuff();
}

Here doFooStuff won't be in the scope even if you somehow manage to extend this. ) Remember, the scope resolution doesn't involve the context object.

So what's the solution? Well, I often use aggregation in similar cases:

var foo = (function(){
    function doFooStuff(){
        console.log(arguments.callee.name);
    }
    return {
        doFooStuff: doFooStuff
    }
})();

var bar = (function(){
    var _super = foo;
    function doBarStuff(){
        console.log(arguments.callee.name);
        _super.doFooStuff();
    }
    // static parent: further changes on `foo` won't affect `bar`,
    // as $.extend takes the parent's current state
    return $.extend({}, _super, {
        doBarStuff: doBarStuff,
    });
    // dynamic parent: further changes on `foo` will affect `bar`,
    // as extended object now has `foo` in its prototype chain
    return $.extend(Object.create(_super), {
        doBarStuff: doBarStuff,
    });
})();

JS Fiddle.

Yes, it's aggregation, not inheritance. But so what? I'm still able to get the main prize - removing code duplication AND I'm able to control the usage of parent functions within the child module.

raina77ow
  • 103,633
  • 15
  • 192
  • 229
  • I see. This wouldn't merge `foo` and `bar` though. – Johan Sep 15 '13 at 20:43
  • Check the update (and the fiddle). Is it what you're looking for? – raina77ow Sep 15 '13 at 20:50
  • Nice, didn't think about extending at the return statement. Why do you pass an empty object as the first argument to `extend`? – Johan Sep 15 '13 at 20:53
  • Because otherwise `foo` object (referred by `_super`within the function) will be changed too. Note that in this case further changes on `foo` will not affect `bar` (it returns a new object based on _current_ `foo` state). If you want it other way, use `return $.extend(Object.create(_super), {..})`, as in @Bergi example. – raina77ow Sep 15 '13 at 20:59
1

The problem with your code is that this in $.extend(this,foo) refers to the window object and not foo. Anonymous function are run in window's context.

I would recommend using a John Resig implementation of classical inheritance in javascript. http://ejohn.org/blog/simple-javascript-inheritance/.

Extract:

var Person = Class.extend({
 init: function(isDancing){
 this.dancing = isDancing;
 },
dance: function(){
 return this.dancing;
 }
});

var Ninja = Person.extend({
 init: function(){
  this._super( false );
 },
dance: function(){
 // Call the inherited version of dance()
 return this._super();
},
 swingSword: function(){
  return true;
 }
});

var p = new Person(true);
p.dance(); // => true

var n = new Ninja();
n.dance(); // => false
n.swingSword(); // => true

// Should all be true
p instanceof Person && p instanceof Class &&
n instanceof Ninja && n instanceof Person && n instanceof Class
Selvam Palanimalai
  • 1,550
  • 1
  • 10
  • 13
0

It is because this is an anonymous function. This part of your function deceleration:

   var foo = (function(){

    function doFooStuff(){
        console.log(arguments.callee.name);
    }

    return {

        doFooStuff: doFooStuff
      }

   })();

Do you see that you are immediately invoking this function call and ultimately do not have access to doFooStuff within your return statement.

If you remove the anonymous function deceleration, it will work.

Ryan
  • 14,392
  • 8
  • 62
  • 102