Understanding better Javascript OOP architecture [duplicate] - javascript

This question already has answers here:
JavaScript: Class.method vs. Class.prototype.method
(5 answers)
Closed 9 years ago.
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

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

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

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.

Related

Am I missing something basic, or is there a bug with Chrome? [duplicate]

I've created a Javascript object via prototyping. I'm trying to render a table dynamically. While the rendering part is simple and works fine, I also need to handle certain client side events for the dynamically rendered table. That, also is easy. Where I'm having issues is with the "this" reference inside of the function that handles the event. Instead of "this" references the object, it's referencing the element that raised the event.
See code. The problematic area is in ticketTable.prototype.handleCellClick = function():
function ticketTable(ticks)
{
// tickets is an array
this.tickets = ticks;
}
ticketTable.prototype.render = function(element)
{
var tbl = document.createElement("table");
for ( var i = 0; i < this.tickets.length; i++ )
{
// create row and cells
var row = document.createElement("tr");
var cell1 = document.createElement("td");
var cell2 = document.createElement("td");
// add text to the cells
cell1.appendChild(document.createTextNode(i));
cell2.appendChild(document.createTextNode(this.tickets[i]));
// handle clicks to the first cell.
// FYI, this only works in FF, need a little more code for IE
cell1.addEventListener("click", this.handleCellClick, false);
// add cells to row
row.appendChild(cell1);
row.appendChild(cell2);
// add row to table
tbl.appendChild(row);
}
// Add table to the page
element.appendChild(tbl);
}
ticketTable.prototype.handleCellClick = function()
{
// PROBLEM!!! in the context of this function,
// when used to handle an event,
// "this" is the element that triggered the event.
// this works fine
alert(this.innerHTML);
// this does not. I can't seem to figure out the syntax to access the array in the object.
alert(this.tickets.length);
}
You can use bind which lets you specify the value that should be used as this for all calls to a given function.
var Something = function(element) {
this.name = 'Something Good';
this.onclick1 = function(event) {
console.log(this.name); // undefined, as this is the element
};
this.onclick2 = function(event) {
console.log(this.name); // 'Something Good', as this is the binded Something object
};
element.addEventListener('click', this.onclick1, false);
element.addEventListener('click', this.onclick2.bind(this), false); // Trick
}
A problem in the example above is that you cannot remove the listener with bind. Another solution is using a special function called handleEvent to catch any events:
var Something = function(element) {
this.name = 'Something Good';
this.handleEvent = function(event) {
console.log(this.name); // 'Something Good', as this is the Something object
switch(event.type) {
case 'click':
// some code here...
break;
case 'dblclick':
// some code here...
break;
}
};
// Note that the listeners in this case are this, not this.handleEvent
element.addEventListener('click', this, false);
element.addEventListener('dblclick', this, false);
// You can properly remove the listners
element.removeEventListener('click', this, false);
element.removeEventListener('dblclick', this, false);
}
Like always mdn is the best :). I just copy pasted the part than answer this question.
You need to "bind" handler to your instance.
var _this = this;
function onClickBound(e) {
_this.handleCellClick.call(cell1, e || window.event);
}
if (cell1.addEventListener) {
cell1.addEventListener("click", onClickBound, false);
}
else if (cell1.attachEvent) {
cell1.attachEvent("onclick", onClickBound);
}
Note that event handler here normalizes event object (passed as a first argument) and invokes handleCellClick in a proper context (i.e. referring to an element that was attached event listener to).
Also note that context normalization here (i.e. setting proper this in event handler) creates a circular reference between function used as event handler (onClickBound) and an element object (cell1). In some versions of IE (6 and 7) this can, and probably will, result in a memory leak. This leak in essence is browser failing to release memory on page refresh due to circular reference existing between native and host object.
To circumvent it, you would need to either a) drop this normalization; b) employ alternative (and more complex) normalization strategy; c) "clean up" existing event listeners on page unload, i.e. by using removeEventListener, detachEvent and elements nulling (which unfortunately would render browsers' fast history navigation useless).
You could also find a JS library that takes care of this. Most of them (e.g.: jQuery, Prototype.js, YUI, etc.) usually handle cleanups as described in (c).
Also, one more way is to use the EventListener Interface (from DOM2 !! Wondering why no one mentioned it, considering it is the neatest way and meant for just such a situation.)
I.e, instead of a passing a callback function, You pass an object which implements EventListener Interface. Simply put, it just means you should have a property in the object called "handleEvent" , which points to the event handler function. The main difference here is, inside the function, this will refer to the object passed to the addEventListener. That is, this.theTicketTable will be the object instance in the belowCode. To understand what I mean, look at the modified code carefully:
ticketTable.prototype.render = function(element) {
...
var self = this;
/*
* Notice that Instead of a function, we pass an object.
* It has "handleEvent" property/key. You can add other
* objects inside the object. The whole object will become
* "this" when the function gets called.
*/
cell1.addEventListener('click', {
handleEvent:this.handleCellClick,
theTicketTable:this
}, false);
...
};
// note the "event" parameter added.
ticketTable.prototype.handleCellClick = function(event)
{
/*
* "this" does not always refer to the event target element.
* It is a bad practice to use 'this' to refer to event targets
* inside event handlers. Always use event.target or some property
* from 'event' object passed as parameter by the DOM engine.
*/
alert(event.target.innerHTML);
// "this" now points to the object we passed to addEventListener. So:
alert(this.theTicketTable.tickets.length);
}
This arrow syntax works for me:
document.addEventListener('click', (event) => {
// do stuff with event
// do stuff with this
});
this will be the parent context and not the document context.
With ES6, you can use an arrow function as that will use lexical scoping[0] which allows you to avoid having to use bind or self = this:
var something = function(element) {
this.name = 'Something Good';
this.onclick1 = function(event) {
console.log(this.name); // 'Something Good'
};
element.addEventListener('click', () => this.onclick1());
}
[0] https://medium.freecodecamp.org/learn-es6-the-dope-way-part-ii-arrow-functions-and-the-this-keyword-381ac7a32881
According to https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener ,
my_element.addEventListener('click', (e) => {
console.log(this.className) // WARNING: `this` is not `my_element`
console.log(e.currentTarget === this) // logs `false`
})
so if you use the arrow functions you can go safe beacause they do not have their own this context.
I know this is an older post, but you can also simply assign the context to a variable self, throw your function in an anonymous function that invokes your function with .call(self) and passes in the context.
ticketTable.prototype.render = function(element) {
...
var self = this;
cell1.addEventListener('click', function(evt) { self.handleCellClick.call(self, evt) }, false);
...
};
This works better than the "accepted answer" because the context doesn't need to be assigned a variable for the entire class or global, rather it's neatly tucked away within the same method that listens for the event.
What about
...
cell1.addEventListener("click", this.handleCellClick.bind(this));
...
ticketTable.prototype.handleCellClick = function(e)
{
alert(e.currentTarget.innerHTML);
alert(this.tickets.length);
}
e.currentTarget points to the target which is bound to the "click event" (to the element that raised the event) while
bind(this) preserves the outerscope value of this inside the click event function.
If you want to get an exact target clicked, use e.target instead.
Heavily influenced by kamathln and gagarine's answer I thought I might tackle this.
I was thinking you could probably gain a bit more freedom if you put handeCellClick in a callback list and use an object using the EventListener interface on the event to trigger the callback list methods with the correct this.
function ticketTable(ticks)
{
// tickets is an array
this.tickets = ticks;
// the callback array of methods to be run when
// event is triggered
this._callbacks = {handleCellClick:[this._handleCellClick]};
// assigned eventListenerInterface to one of this
// objects properties
this.handleCellClick = new eventListenerInterface(this,'handleCellClick');
}
//set when eventListenerInterface is instantiated
function eventListenerInterface(parent, callback_type)
{
this.parent = parent;
this.callback_type = callback_type;
}
//run when event is triggered
eventListenerInterface.prototype.handleEvent(evt)
{
for ( var i = 0; i < this.parent._callbacks[this.callback_type].length; i++ ) {
//run the callback method here, with this.parent as
//this and evt as the first argument to the method
this.parent._callbacks[this.callback_type][i].call(this.parent, evt);
}
}
ticketTable.prototype.render = function(element)
{
/* your code*/
{
/* your code*/
//the way the event is attached looks the same
cell1.addEventListener("click", this.handleCellClick, false);
/* your code*/
}
/* your code*/
}
//handleCellClick renamed to _handleCellClick
//and added evt attribute
ticketTable.prototype._handleCellClick = function(evt)
{
// this shouldn't work
alert(this.innerHTML);
// this however might work
alert(evt.target.innerHTML);
// this should work
alert(this.tickets.length);
}
The MDN explanation gives what to me is a neater solution further down.
In this example you store the result of the bind() call, which you can then use to unregister the handler later.
const Something = function(element) {
// |this| is a newly created object
this.name = 'Something Good';
this.onclick1 = function(event) {
console.log(this.name); // undefined, as |this| is the element
};
this.onclick2 = function(event) {
console.log(this.name); // 'Something Good', as |this| is bound to newly created object
};
// bind causes a fixed `this` context to be assigned to onclick2
this.onclick2 = this.onclick2.bind(this);
element.addEventListener('click', this.onclick1, false);
element.addEventListener('click', this.onclick2, false); // Trick
}
const s = new Something(document.body);
In the posters example you would want to bind the handler function in the constructor:
function ticketTable(ticks)
{
// tickets is an array
this.tickets = ticks;
this.handleCellClick = this.handleCellClick.bind(this); // Note, this means that our handleCellClick is specific to our instance, we aren't directly referencing the prototype any more.
}
ticketTable.prototype.render = function(element)
{
var tbl = document.createElement("table");
for ( var i = 0; i < this.tickets.length; i++ )
{
// create row and cells
var row = document.createElement("tr");
var cell1 = document.createElement("td");
var cell2 = document.createElement("td");
// add text to the cells
cell1.appendChild(document.createTextNode(i));
cell2.appendChild(document.createTextNode(this.tickets[i]));
// handle clicks to the first cell.
// FYI, this only works in FF, need a little more code for IE
this.handleCellClick = this.handleCellClick.bind(this); // Note, this means that our handleCellClick is specific to our instance, we aren't directly referencing the prototype any more.
cell1.addEventListener("click", this.handleCellClick, false);
// We could now unregister ourselves at some point in the future with:
cell1.removeEventListener("click", this.handleCellClick);
// add cells to row
row.appendChild(cell1);
row.appendChild(cell2);
// add row to table
tbl.appendChild(row);
}
// Add table to the page
element.appendChild(tbl);
}
ticketTable.prototype.handleCellClick = function()
{
// PROBLEM!!! in the context of this function,
// when used to handle an event,
// "this" is the element that triggered the event.
// this works fine
alert(this.innerHTML);
// this does not. I can't seem to figure out the syntax to access the array in the object.
alert(this.tickets.length);
}

javascript namespace call other function methods

I try to change some way to call methods into namespace.
Calling parent methods (I dont think its possible)
Creating and call inheritance function
Calling inside another method (mostly jquery onReady event function) (this.MyFunction() not working)
I split every namespace in files (want to keep it that way)
I try How to call function A from function B within the same namespace? but I didn't succed to split namespaces.
my fiddle sample got only 1 sub-namespace but could be more.
https://jsfiddle.net/forX/kv1w2rvc/
/**************************************************************************
// FILE Master.js
***************************************************************************/
if (!Master) var Master = {};
Master.Print= function(text){
console.log("master.Print :" + text);
$("body").append("<div>master.Print : " + text + "</div>");
}
/**************************************************************************
// FILE Master.Test1.js
***************************************************************************/
if (!Master) var Master = {};
if (!Master.Test1) Master.Test1 = {};
/**************************************************************************
* Descrition :
* Function for managing event load/documentReady
**************************************************************************/
Master.Test1.onReady = function () {
$(function () {
Master.Test1.Function1(); //try to replace because need all namespace.
try {
this.Function2(); //not working
}
catch(err) {
console.log("this.Function2 not working");
$("body").append("<div>this.Function2 not working</div>");
}
try {
this.Print("onReady"); //not working
}
catch(err) {
console.log("this.Print not working");
$("body").append("<div>this.Print not working</div>");
}
try {
Print("onReady"); //not working
}
catch(err) {
console.log("Print not working");
$("body").append("<div>Print not working</div>");
}
});
}
Master.Test1.Function1 = function () {
console.log("Function1");
$("body").append("<div>Function1</div>");
this.Function3(); //working because not inside another function
}
Master.Test1.Function2 = function () {
$("body").append("<div>Function2</div>");
console.log("Function2");
}
Master.Test1.Function3 = function () {
$("body").append("<div>Function3</div>");
console.log("Function3");
Master.Print("Function3"); //try to replace because need all namespace.
}
Master.Test1.onReady();
I use Master.Test1.Function1(); and I want to change that because Function1 is inside the same namespace.
I use Master.Print("Function3"); I dont think I can change that. the way I try to use it, it's more an inheritance function. but I dont know if theres a way to do that?
Maybe I should change the my namespace methode? maybe prototype will do what I want?
You can capture the this in a variable because this inside $(function() {}) will point to document object. The below will work provided you never change the calling context of onReady -- i.e. it is always called on the Test1 object and not called on other context:
Master.Test1.onReady = function () {
var self = this;
$(function () {
self.Function1();
// ..
});
}
To access Print you have to reference using the Master object like: Master.Print() as it won't be available in the Test1 object
this is document within .ready() or jQuery() alias for .ready() where function(){} is parameter $(function() {}). this at this.Function2() will reference document.
"Objects" in javascript are not built the same way as in most object-oriented languages. Essentially, what you are building is a hierarchy of static methods that have no real internal state in-and-of themselves. Therefore, when one of the defined methods is invoked, the context (or state) of that method depends on what object invoked the method.
If you want to have any internal context, you will need to create an "instance" of an "object prototype". At that point, you can use "this.otherFunction" within your other functions. Here is a small example:
var MyObject = function() {};
MyObject.functionOne = function() {
console.log("Function 1");
this.functionTwo();
};
MyObject.functionTwo = function() {
console.log("Function 2");
};
var instanceOne = new MyObject();
instanceOne.functionOne();
You might get some more information about object definition here

Why define event handler member functions (inline) inside the constructor function in order to work with 'unbind'?

While studying the source code for a signature pad widget, I find the following code snippet inside the constructor function (note in particular the comment in the following snippet):
var SignaturePad = function (canvas, options) {
...
// MY QUESTION IS ABOUT THE FOLLOWING CODE COMMENT!!!
// v v v
// we need add these inline so they are available to unbind while still having
// access to 'self' we could use _.bind but it's not worth adding a dependency
this._handleMouseDown = function (event) {
if (event.which === 1) {
self._mouseButtonDown = true;
self._strokeBegin(event);
}
};
// ... other event handlers here
...
}
... for completeness in providing context for the above code, later the event handlers are bound as event listeners:
SignaturePad.prototype._handleMouseEvents = function () {
...
this._canvas.addEventListener("mousedown", this._handleMouseDown);
...
};
From the above code snippet, you can see the comment:
we need add these inline so they are available to unbind while still having access to 'self'
we could use _.bind but it's not worth adding a dependency`
I am scratching my head about this. Why is access self required when unbinding (and I assume by 'unbinding' is meant detaching the event listener, but please correct me if I'm wrong)?
In other words, I'd like to understand the above code comment so that I can be certain I understand the JavaScript and/or event binding thoroughly in this code.
The .addEventListener calls in that code receive a function reference when binding the handler. In order to use .removeEventListener to unbind, you need to pass a reference to the same function handler.
Because the SignaturePad constructor creates a new, unique (though identical) function for each instance, and binds that function, they need to keep a reference to that function in order to unbind later on. Therefore they put it directly on the object for later use.
The reason they create these handlers inside the constructor function is that they want them to be able to reference the SignaturePad instance that was created. So they create a var self = this variable, and have the functions created in the constructor reference self. If the handlers were on the .prototype, there would be no way for that shared handler to reference the original object, given their approach.
Here's a truncated version of their code that shows how to use the EventListener interface:
var SignaturePad = function(canvas, options) {
this._handleMouseEvents();
};
// Implements the EventListener interface
SignaturePad.prototype.handleEvent = function(event) {
switch (event.type) {
case "mousedown":
this._handleMouseDown(event)
break
case "mousemove":
this._handleMouseMove(event)
break
case "mouseup":
this._handleMouseUp(event)
break
default:
console.log("Unbound event type:", event.type)
}
}
SignaturePad.prototype._handleMouseDown = function(event) {
if (event.which === 1) {
this._mouseButtonDown = true;
this._strokeBegin(event);
}
};
SignaturePad.prototype._handleMouseMove = function(event) {
if (this._mouseButtonDown) {
this._strokeUpdate(event);
}
};
SignaturePad.prototype._handleMouseUp = function(event) {
if (event.which === 1 && this._mouseButtonDown) {
this._mouseButtonDown = false;
this._strokeEnd(event);
}
};
SignaturePad.prototype._strokeUpdate = function(event) {
console.log("stroke update");
};
SignaturePad.prototype._strokeBegin = function(event) {
console.log("stroke begin");
};
SignaturePad.prototype._strokeEnd = function(event) {
console.log("stroke end");
};
SignaturePad.prototype._handleMouseEvents = function() {
this._mouseButtonDown = false;
this._canvas.addEventListener("mousedown", this);
this._canvas.addEventListener("mousemove", this);
document.addEventListener("mouseup", this);
};
So you can see that the handleEvent method was added, and we don't actually bind any functions using .addEventListener. Instead, we bind a reference to the SignaturePad object itself.
When an event occurs, the handleEvent method is invoked with the value of this pointing our SignaturePad object we bound. We still have access to the element as well via event.currentTarget.
So this lets us reuse functions on the .prototype and gives us all the object references we need. And of course unbinding is done the same way, except that we pass the object we bound to .removeEventListener.

Javascript object methods and binding functions

I'm looking for some help because I don't quite think I understand the Javascript scoping rules. What I'm trying to do in the below example is to push a button on a page that then starts listening for keyboard input. Once the keyboard input has started if there is a break in input for two seconds I want to stop capturing the input and pop an alert with the full contents of the input collected to that point. This is an example I made purely for this question.
What I see is that I click the button and start entering input. On each keypress I am alerted to the string collected to that point. After the two second, no-action timeout takes place I see an alert with the contents "undefined". The first alerts listed above come from startLog(). The second alert comes from stopLog(). What am I doing wrong when I call stopLog that it is telling me that this.message is undefined?
function Logger() {
this.message = '';
this.listenTimer;
this.startLog = function() {
this.message = '';
$(document).bind('keypress', {this_obj:this}, function(event) {
event.preventDefault();
var data = event.data;
clearTimeout(data.this_obj.listenTimer);
data.this_obj.message += String.fromCharCode(event.which);
alert(data.this_obj.message);
data.this_obj.listenTimer = setTimeout(data.this_obj.stopLog, 2000);
});
};
this.stopLog = function() {
$(document).unbind("keypress");
alert(this.message);
};
}
var k = new Logger();
$('.logging-button').click(function() {
k.startLog();
});
The issue is this. When you pass an object method as an event handler, it loses its object context; this will refer to the window object.
There are various ways to fix this, but the main issue is that you need to pass setTimeout a closure that will still refer to the correct context:
setTimeout(function() { data.this_obj.stopLog() }, 2000);
On a separate note, you can save yourself some unnecessary code by just using a closure to refer to the object, rather than binding it as event.data:
this.startLog = function() {
this.message = '';
var this_obj = this;
$(document).bind('keypress', function(event) {
event.preventDefault();
clearTimeout(this_obj.listenTimer);
// etc
});
};
var k = new Logger();
$('.logging-button').click(function() {
k.startLog.apply(this);
//Setting context of "this" so that it refers to element even in startLog()
});

"this" doesn't work when binding functions to events inside a Javascript class

First off, I know I can copy "this" on instantiation, but that doesn't work here.
Basically I'm writing something to track people interacting with Youtube videos.
I got this working fine for one video at a time. But I want it to work on pages with multiple Youtube videos as well, so I converted the code to a class so I can create a new instance of it for each video on the page.
The problem is when trying to bind to the Youtube event listener for state changes. For "non-class" code, it looks like this:
var o = document.getElementById( id );
o.addEventListener("onStateChange", "onPlayerStateChange" );
(onPlayerStateChange being the function I wrote to track state changes in the video)
(I'm also aware that addEventListener won't work with MSIE but I'm not worrying about that yet)
But when I'm inside a class, I have to use "this" to refer to another function in that class. Here's what the code looks like:
this.o = document.getElementById( id );
this.o.addEventListener("onStateChange", "this.onPlayerStateChange" );
When it's written like this, this.onPlayerStateChange is never called. I've tried copying "this" into another variable, e.g. "me", but that doesn't work either. The onPlayerStateChange function is defined within the "this" scope before I do this:
var me = this;
this.o = document.getElementById( id );
this.o.addEventListener("onStateChange", "me.onPlayerStateChange" );
Any insights?
Looking through other similar questions here, all of them are using jQuery, and I think doing it that way might work if I did it that way. But I don't want to use jQuery, because this is going to be deployed on random third party sites. I love jQuery but I don't want it to be a requirement to use this.
You need a global way to access the onPlayerStateChange method of your object. When you assign me as var me = this;, the variable me is only valid inside the object method where it is created. However, the Youtube player API requires a function that is accessible globally, since the actual call is coming from Flash and it has no direct reference to your JavaScript function.
I found a very helpful blog post by James Coglan in which he discussed a nice way to communicate with the Youtube's JavaScript API and manage events for multiple videos.
I have released a JavaScript wrapper library using his ideas at http://github.com/AnuragMishra/YoutubePlayer. Feel free to checkout the code. The underlying idea is simple - store all instances of the player object on the constructor. For example:
function Player(id) {
// id of the placeholder div that gets replaced
// the <object> element in which the flash video resides will
// replace the placeholder div and take over its id
this.id = id;
Player.instances.push(this);
}
Player.instances = [];
When passing a string as a callback, use a string of the form:
"Player.dispatchEvent('playerId')"
When the flash player evals this string, it should return a function. That function is the callback that will ultimately receive the playback event id.
Player.dispatchEvent = function(id) {
var player = ..; // search player object using id in "instances"
return function(eventId) { // this is the callback that Flash talks to
player.notify(eventId);
};
};
When the flash player has loaded the video, the global onYoutubePlayerReady function is called. Inside that method, setup the event handlers for listening to playback events.
function onYouTubePlayerReady(id) {
var player = ..; // find player in "instances"
// replace <id> with player.id
var callback = "YoutubePlayer.dispatchEvent({id})";
callback = callback.replace("{id}", player.id);
player.addEventListener('onStateChange', callback);
}
See a working example here..
You can use a technique called currying to achieve this. For that you need a currying function. Here's one I wrote some time back
/**
* Changes the scope of function "fn" to the "scope" parameter specified or
* if not, defaults to window scope. The scope of the function determines what
* "this" inside "fn" evaluates to, inside the function "fn". Any additional arguments
* specified in this are passed to the underlying "curried" function. If the underlying
* function is already passed some arguments, the optional arguments are appended
* to the argument array of the underlying function. To explain this lets take
* the example below:
*
* You can pass any number of arguments that are passed to the underlying (curried)
* function
* #param {Function} fn The function to curry
* #param {Object} scope The scope to be set inside the curried function, if
* not specified, defaults to window
* #param arguments {...} Any other optional arguments ot be passed to the curried function
*
*/
var curry = function(fn, scope /*, arguments */) {
scope = scope || window;
var actualArgs = arguments;
return function() {
var args = [];
for(var j = 0; j < arguments.length; j++) {
args.push(arguments[j]);
}
for(var i = 2; i < actualArgs.length; i++) {
args.push(actualArgs[i]);
}
return fn.apply(scope, args);
};
};
You can use it to curry other functions and maintain the 'this' scope inside the functions.
Check out this article on currying
this.o.addEventListener("onStateChange", curry(onPlayerStateChange, this));
Edit:
var curriedFunc = curry(onPlayerStateChange, this);
this.o.addEventListener("onStateChange", "curriedFunc");
Edit:
Okay lets say this is your custom class you create:
function MyCustomClass() {
var privateVar = "x"; // some variables;
this.onPlayerStateChange = function() { //instance method on your custom class
// do something important
}
}
On a global level you create an instance of MyCustomClass
var myCustom = new MyCustomClass(); // create a new instance of your custom class
var curriedFunc = curry(myCustom.onplayerStageChange, myCustom); // curry its onplayerstateChange
// now add it to your event handler
o.addEventListener("onStateChange", "curriedFunc");
You should be using the following to attach an event:
this.o.addEventListener("statechange", this.onPlayerStateChange);
For addEventListener, you don't need to add the on prefix.
What I posted above is correct for standard javascript, but because this passes it to the YT flash object, it's expecting onStateChange which is correct.
HTH
EDIT: Try the method in this post to help.
TheCloudlessSky was partly right and Sean was partly right. You can continue to use "onStateChange" as the event name, but don't put this.onPlayerStateChange in quotations - doing so removes the special meaning of this and javascript will look for a function named "this.onPlayerStateChange" rather than looking for a function "onPlayerStateChange" within this object.
this.o.addEventListener("onStateChange", this.onPlayerStateChange);
After looking at the Youtube Api, it looks like the addEventListener only accepts a String for the event handler function. That means there's no clean way to register a unique event handler for each object.
An alternative is to register a global handler for all youtube state changes, and then let that handler pass the state change onto all your objects. Assuming you have an array of "tracker" objects:
function globalOnPlayerStateChange() {
for (tracker in myTrackerObjects) {
tracker.playerStateChange();
}
}
Each tracker object can then figure out by itself whether or not a state change actually occured (using the API's getPlayerState function):
function MyYoutubeTracker() {
this.currentState = ...
// Determine if state changed happened or not
this.playerStateChange = function() {
var newState = this.o.getPlayerState();
if (newState != this.currentState) {
// State has changed
this.currentState = newState;
}
}
// Register global event handler for this youtube object
this.o.addEventListener("onStateChange", "globalOnPlayerStateChange");
}
Ok, I got this all working. It's a bit of an ugly hack but it works. Basically I'm storing each new instance of the class in an array, and I'm passing the array key (1, 2, etc) into the class, so it can refer to itself externally as needed in a few key places.
The places I need the class to refer to itself externally are the string I pass to addEventListener, and within a few setTimeout functions, where "this" apparently loses its context (as far as I can tell anyways, because the only way I could them working was changing "this" to use external references instead.
Here's the full code.
On the page that has Youtube videos, they are injected using swfobject. The _ytmeta object stores the titles for each video. It's optional, but it's the only way to log the title of a video, because Youtube's API does not give it to you. This means you have to know the title up front, but the point is simply that if you want the title to show up in our reports, you have to create this object:
<div id='yt1'></div>
<script src='youtube.js'></script>
<script src='swfobject.js'></script>
<script>
var _ytmeta = {}
_ytmeta.yt1 = { 'title': 'Moonwalking in Walmart' };
var params = { allowScriptAccess: "always" };
swfobject.embedSWF("http://www.youtube.com/v/gE1ZvCnwkYk?enablejsapi=1&playerapiid=yt1", "yt1", "425", "356", "8", null, null, params );
</script>
So we're including the swfobject javascript code, as well as the youtube.js file, which will be hosted on our server and included on the pages you want to track videos.
Here are the contents of youtube.js:
// we're storing each youtube object (video) in an array, and passing the array key into the class, so the class instance can refer to itself externally
// this is necessary for two reasons
// first, the event listener function we pass to Youtube has to be globally accessible, so passing "this.blah" doesn't work
// it has to be passed as a string also, so putting "this" in quotes makes it lose its special meaning
// second, when we create timeout functions, the meaning of "this" inside that function loses its scope, so we have to refer to the class externally from there too.
// _yt is the global youtube array that stores each youtube object. yti is the array key, incremented automatically for each new object created
var _yt = [], _yti = 0;
// this is the function the youtube player calls once it's loaded.
// each time it's called, it creates a new object in the global array, and passes the array key into the class so the class can refer to itself externally
function onYouTubePlayerReady( id ) {
_yti++;
_yt[ _yti ] = new _yta( id, _yti );
}
function _yta( id, i ) {
if( !id || !i ) return;
this.id = id;
this.mytime;
this.scrubTimer;
this.startTimer;
this.last = 'none';
this.scrubbing = false;
this.o = document.getElementById( this.id );
this.o.addEventListener("onStateChange", "_yt["+i+"].onPlayerStateChange" );
this.onPlayerStateChange = function( newState ) {
// some events rely on a timer to determine what action was performed, we clear it on every state change.
if( this.myTime != undefined ) clearTimeout( this.myTime );
// pause - happens when clicking pause, or seeking
// that's why a timeout is used, so if we're seeking, once it starts playing again, we log it as a seek and kill the timer that would have logged the pause
// we're only giving it 2 seconds to start playing again though. that should be enough for most users.
// if we happen to log a pause during the seek - so be it.
if( newState == '2' ) {
this.myTime = setTimeout( function() {
_yt[i].videoLog('pause');
_yt[i].last = 'pause';
_yt[i].scrubbing = false;
}, 2000 );
if( this.scrubbing == false ){
this.last = 'pre-scrub';
this.scrubbing = true;
}
}
// play
else if( newState == '1' ) {
switch( this.last ) {
case 'none':
this.killTimers();
this.startTimer = setInterval( this.startRun, 200 );
break;
case 'pause':
this.myTime = setTimeout( function() {
_yt[i].videoLog('play');
_yt[i].last = 'play';
}, 2000 );
break;
case 'pre-scrub':
this.killTimers();
this.scrubTimer = setInterval( this.scrubRun, 200 );
break;
}
}
// end
else if( newState == '0' ) {
this.last = 'none';
this.videoLog('end');
}
}
// have to use external calls here because these are set as timeouts, which makes "this" change context (apparently)
this.scrubRun = function() {
_yt[i].videoLog('seek');
_yt[i].killTimers();
_yt[i].last = 'scrub';
_yt[i].scrubbing = false;
}
this.startRun = function() {
_yt[i].videoLog('play');
_yt[i].killTimers();
_yt[i].last = 'start';
}
this.killTimers = function() {
if( this.startTimer ) {
clearInterval( this.startTimer );
this.startTimer = null;
}
if( this.scrubTimer ){
clearInterval( this.scrubTimer );
this.scrubTimer = null;
}
}
this.videoLog = function( action ) {
clicky.video( action, this.videoTime(), this.videoURL(), this.videoTitle());
}
this.videoTime = function() {
return Math.round( this.o.getCurrentTime() );
}
this.videoURL = function() {
return this.o.getVideoUrl().split('&')[0]; // remove any extra parameters - we just want the first one, which is the video ID.
}
this.videoTitle = function() {
// titles have to be defined in an external object
if( window['_ytmeta'] ) return window['_ytmeta'][ this.id ].title || '';
}
}
Hopefully, someone in the future will find this helpful, because it was a serious pain in the ass to get it working!
Thank you everyone who posted their ideas here. :)

Categories