4

As i read through some examples of Angularjs' UI add-on, i've stumbled over some code that showed me that my knowdledge of Javascript is quite improvable:

The following is a class inside of an Angular provider:

function Dialog(opts) {

            var self = this, options = this.options = angular.extend({}, defaults, globalOptions, opts);
            this._open = false;

            this.backdropEl = createElement(options.backdropClass);
            if(options.backdropFade){
                // ...
            }

            this.handleLocationChange = function() {
                self.close();
            };

            // more functions
        }

Pretty straightforward. But outside of that class, there are prototype functions, e.g the above invoked close()

Dialog.prototype.open = function(templateUrl, controller){
            var self = this, options = this.options;

            // .. some code
        };

Now i do not understand why that function is declared as a prototype, but handleLocationChange inside the class itself.

How do i decide which method to choose?

The full gist can be found here

user2422960
  • 1,476
  • 6
  • 16
  • 28
  • 1
    judging from the code show, both are interchangeable. you may be staring too hard at a couple trees blocking your view of the forest. Coders write different code on mondays than on fridays, and perhaps, like my mom used to say "that's just how god made it". – dandavis Jun 26 '13 at 08:08
  • @dandavis I don't think so, the function is placed inside the constructor body so it can take advantage of the closure value named `self` this is probably because handleLocationChange is passed as a variable for callback at some point so the `this` context get lost. See my answer for full details. I am curious as to why they would add the weight of closure variables and instance owned functions since there are other solutions that don't require consuming extra memory. This might sound trivial but in an application like google docs this could add up. – HMR Jun 26 '13 at 09:25
  • @HMR: well self is the same as this, which is reachable from prototype methods the same as inline methods. it could be wired as this.close() or self.handleLocationChange, and bind or call takes care of any ill-begotten this value at a later point if the need arises. – dandavis Jun 26 '13 at 17:53
  • @dandavis You can't use self in any prototype function becase self is declared as `var self=this` in the constructor function body so self is out of scope anywhere outside the function body (prototype functions are declared outside the function body). This question is not a duplicate of the other one as the self variable is not used as a private, I suspect Angular needs it because their event/Publish/Subscribe system the handleLocationChange is passed without a reference to the particular object. – HMR Jun 26 '13 at 23:53
  • @HMR. all i meant was that in the code shown, self===this. so, in a prototype method, this.x is the same as self.x within the constructor. – dandavis Jun 26 '13 at 23:56
  • @dandavis Sorry, it was unclear. Still wonder why *Angular* would do it this way. The people who duped this question haven't taken the time to look at the question because self is obviously not used for privateness and the dupe doesn't address why *Angular* would declare the functions in the constructor body. The fact that objects using this pattern (instead of defining functions on the prototype) are unnecessarily heavier isn't stressed either, it shows up as a comment somewhere referring to this article: http://code.google.com/intl/es/speed/articles/optimizing-javascript.html – HMR Jun 27 '13 at 00:15

3 Answers3

3

Consider these 2 cases:

Dialog.prototype.open = function...

Dialog.open = function....

First case - every object created by calling new Dialog() will have this open function

Second case has nothing to do with dialog objects, consider it as static function.

EDIT

found a great answer here : javascript-class-method-vs-class-prototype-method

Community
  • 1
  • 1
Tala
  • 8,888
  • 5
  • 34
  • 38
  • handleLocationChange is declared as this.handleLocationChange so every dialog created with new Dialog not only has handleLocationChange it has it's own handleLocationChange instead of sharing it with other instances on the prototype. I'm not sure why they would do that but havent used Angular yet so curious to find out. – HMR Jun 26 '13 at 07:50
  • 1
    I think it is supposed to be overriden by you. Since all it does now - calls close() – Tala Jun 26 '13 at 07:53
  • I think the importaint thing is it calls `self`.close. – HMR Jun 26 '13 at 08:00
2

function open will be shared by all objects create using new Dialog().. and handleLocationChange will be different for different objects.

Mukesh Kumar
  • 783
  • 1
  • 9
  • 24
  • +1 You are right, and I would even add that adding a function to a prototype will cause all the existing (and future) objects of that prototype to "inherit" that function – Nir Alfasi Jun 26 '13 at 07:46
  • 1
    One of those big Ahaaa! moments. Can i say then, that if i would have declared handleLocationChange as a prototype function, it would not close only that one specific dialog? – user2422960 Jun 26 '13 at 07:46
  • 1
    I see, handleLocationChange is a `handler` that responds to events of a specific instance. You could declare it on the prototype but you have to trigger the event on the instance: instanceOfDialog.trigger("locationChange"); My guess is that Angular's event system does't work that way? – HMR Jun 26 '13 at 07:55
  • 1
    @user2422960 it would probably get an Error since `this` doesn't refer to the dialog instance and `self` can only be used in the Dialog body (prototype functions are outside of the Dialog function body). I've updated my answer as to why I think they include the event handler in the body of the constructor function. – HMR Jun 26 '13 at 08:15
2

I think handleLocationChange is called from event triggering object that registers listeners but doesn't register the this context so when it's triggered you can't use this as it refers to handleLocationChange. To overcome this they have chosen to set a closure reference to this (=the self variable) and call other instance functions using self. Basically it's storing a value known at creation but not known when handleLocationChange is executing.

Here is some code showing the problem:

var eventSystem={
  events:{},
  add:function(eventname,fnCallback){
     if(!this.events[eventname]){
      this.events[eventname]=[];
     }
     this.events[eventname].push(fnCallback);
  },
  trigger:function(eventname){
    if(!this.events[eventname]){
      return;
    }
    var i=0;
    for(i=0;i<this.events[eventname].length;i++){
      this.events[eventname][i]();
    }
  }
};

var person=function(name){
  this.name=name;
};
person.prototype.sayName=function(){
  console.log("this is now:",this.toString());
    // logs this is now: function (){ console.log("this is now:...
    // so this is now the sayName function not the person instance
  console.log(this.name);//undefined: sayName doesn't have a name property
}
var jon=new person("jon");
eventSystem.add("sayname",jon.sayName);//add event and listener function
eventSystem.trigger("sayname");//trigger the event

Here is how it's solved setting a closure reference

var eventSystem={
  events:{},
  add:function(eventname,fnCallback){
     if(!this.events[eventname]){
      this.events[eventname]=[];
     }
     this.events[eventname].push(fnCallback);
  },
  trigger:function(eventname){
    if(!this.events[eventname]){
      return;
    }
    var i=0;
    for(i=0;i<this.events[eventname].length;i++){
      this.events[eventname][i]();
    }
  }
};

var person=function(name){
  var self=this;// set closure ref to this
  this.name=name;
  this.sayName=function(){
    console.log(self.name);//use closure ref to get this
       // logs jon
  }
};
var jon=new person("jon");
eventSystem.add("sayname",jon.sayName);//add event and listener function
eventSystem.trigger("sayname");//trigger the event

Here is a fix to the event system to take care of the this context:

var eventSystem={
  events:{},
  add:function(eventname,fnCallback,thisRef){
     if(!this.events[eventname]){
      this.events[eventname]=[];
     }
     this.events[eventname].push({
       "callback":fnCallback,//store the event handler
       "thisRef":thisRef//store the this context
     });
  },
  trigger:function(eventname){
    if(!this.events[eventname]){
      return;
    }
    var i=0;
    for(i=0;i<this.events[eventname].length;i++){
      this.events[eventname][i].callback.call(
        this.events[eventname][i].thisRef);
    }
  }
};

var person=function(name){
  this.name=name;
};
person.prototype.sayName=function(){
  console.log("this is now:",this);//referring to person instance
    // with the name jon
  console.log(this.name);//logs jon
  console.log(this instanceof person);//true
}


var jon=new person("jon");
eventSystem.add("sayname",jon.sayName,jon);//add extra parameter for this ref
eventSystem.trigger("sayname");//trigger the event

The pattern used above is not an event system (think it's pulisher subscriber) as an event usually get triggered on or invoked from an object (button, input, dialog) but in case of a more event system like implementation it would be easy to get the correct this context since you trigger the event on or from an instance (like myButton or myDialog).

See following code for event system like implementation:

var eventSystem={
  add:function(eventname,fnCallback){
     if(!this.events[eventname]){
      this.events[eventname]=[];
     }
     this.events[eventname].push(fnCallback);
  },
  //change in trigger as it's passing the event object now
  trigger:function(event){
    if(!this.events[event.type]){
      return;
    }
    var i=0;
    for(i=0;i<this.events[event.type].length;i++){
      this.events[event.type][i](event);
    }
  },
  initES:function(){//set the instance variables needed
    this.events=this.events||{};
  }
};
function addProtos(o,protos){
  for(item in protos){
    o.prototype[item]=protos[item];
  }
}
var person=function(name){
  this.name=name;
  this.initES();//needed to initialeze eventsystem
};
// make person capable of storing event handlers
// and triggering them
addProtos(person,eventSystem);
person.prototype.askQuestion=function(){
  //asking a question will trigger an "answer" event
  this.trigger({type:"answer",target:this});
}
// handler for when jon will fire an answer event
function answerHandler(event){
  console.log("answer from:",event.target);
  console.log("name of the person:",event.target.name);
}

var jon=new person("jon");
jon.add("answer",answerHandler);//add event listener
jon.askQuestion();//triggers the answer event from within jon
jon.trigger({type:"answer",target:jon});//trigger the event externally

Not sure why Angular choose to "break" prototype by using closures as the examples show there are other alternatives. Maybe someone can explain that who is more familiar with Angular.

BenMorel
  • 34,448
  • 50
  • 182
  • 322
HMR
  • 37,593
  • 24
  • 91
  • 160