Javascript closure scope - javascript

I was working today at a front-end Javascript project. I will try to keep the description of the problem and the solution as short as possible.
I had to add click handlers to the links on a page that redirect the user to other pages, so I had 2 Javascript array arrayOfRedirectLinks and pageLinkElements:
var arrayOfRedirectLinks, pageLinkElements;
Initially I wrote the addEventHandlers function like this:
var addEventHandlers = function() {
var i, link;
for( var i in arrayOfRedirectLinks) {
link = arrayOfRedirectLinks[i];
pageLinkElements[i].addEventListener('click', function(e) {
e.preventDefault();
window.location = link;
});
}
}
I thought that this solution will do the job until... well until I opened the browser, clicked several links and noticed that all of them redirected me to the same link (the last link in the arrayOfRedirectLinks).
Finally I found that my problem was similar to the one posted here
Javascript multiple dynamic addEventListener created in for loop - passing parameters not working
And indeed both the first and the second solution posted there worked for me
var addEventHandlers = function() {
var i, link;
for( var i in arrayOfRedirectLinks) {
(function(link){
link = arrayOfRedirectLinks[i];
pageLinkElements[i].addEventListener('click', function(e) {
e.preventDefault();
window.location = link;
});
}(link));
}
}
and
var passLink = function(link) {
return function(e) {
e.preventDefault();
window.location = link;
};
};
var addEventHandlers = function() {
var i, link;
for( var i in arrayOfRedirectLinks) {
link = arrayOfRedirectLinks[i];
pageLinkElements[i].addEventListener('click',passLink(link));
}
}
Now this seems to work but I don't understand why it works.
I came with the following explanation and I would like if someone can confirm if it's correct:
When I declare a function in Javascript, it gets the references to the variables in the scope of the function where it was declared. ( i.e. my event handler gets a reference to the link variable in the addEventHandlers function)
Because the handler gets a reference to the variable link. When I reassign a value to the link variable, the value that will be used when the click handler gets triggered will also change. So the link variable from the event handler is not simply copy with of the link with a different memory address and same value as when the function handler was added, but they both share the same memory address and therefore the same value.
Because of the reasons described at 2), the all the click handlers will use the redirect to the same link, the last link in the array arrayOfRedirectLinks because that's the last value that will get assigned to the link variable at the end of the for loop.
But when I pass the link variable as a parameter to another function, a new scope it's created and the link inside that scope actually shares only it's initial value with the value of the link parameter passed to the function. The references of the 2 link variables are different.
Because of 4), when I pass the link to the click handler, it will take the reference to the link variable in the Immediately Invoked Function Expression who itself doesn't share the same address with the link in the addEventHandlers function. Therefore each link from the event handler functions will be isolated from the others and will keep the value of the arrayOfRedirectLinks[i]
Is this correct?

This is the critical bit:
var i, link;
for( var i in arrayOfRedirectLinks) {
link = arrayOfRedirectLinks[i];
pageLinkElements[i].addEventListener('click', function(e) {
e.preventDefault();
window.location = link;
});
}
There is only ever one link variable here. Note that the addEventListener callback function is only called when the link is clicked.
By that time, the variable link has its final value, which is shared by all event handler functions.
So all the links do the same thing.
Simplest solution (other than a wider refactor):
for(var i in arrayOfRedirectLinks) {
(function(i) {
var link = arrayOfRedirectLinks[i];
pageLinkElements[i].addEventListener('click', function(e) {
e.preventDefault();
window.location = link;
});
}(i));
}

Related

Why is my method not seeing this.property?

In the code below, initializeBoard has access to the property, and the console returns 'white' when I start the script. But when I click inside the window, I get 'undefined'. What obvious thing am I missing? (Bonus: what's the search query that'd have led me to the answer without having to ask?)
var view = {
currentMove: 'white',
initializeBoard: function() {
console.log(this.currentMove);
},
click: function(e) {
console.log(this.currentMove);
}
}
window.onload = function() {
view.initializeBoard();
document.onclick = view.click;
}
The value of this is determined by how the function is called, not by where it is first assigned.
You are copying the (reference to the) function to document.onclick.
When the click event happens document.onclick is called. view.click is not called (even though it has the same value as document.onclick). This means that this is document not view.
Use bind if you want to create a wrapper function that calls the original function in the right context.
document.onclick = view.click.bind(view);

Storing previous event.target.id in a variable

I am in a situation where I need to keep track of the previously clicked event.target.id that is firing the click event.
A very simple example of my code is as follows (I am using dojo and jQuery):
on(dom.byId("div-tools-draw"), "click", function (evt) {
var lastActiveTool = evt.target.id;
}
This code keeps overwriting the lastActiveTool variable with the current event id. However, I need a way to keep track of the previous one.
Sorry if this is a silly question, I am still learning JS.
var lastActiveTool;
on(dom.byId("div-tools-draw"), "click", function (evt) {
//do whatever you want with previous value if there is one
lastActiveTool = evt.target.id;
}
Firstly you shouldn't declare your variable inside the function as it'll only be accessible inside that function and since it's an anonymous function, it'll be destroyed each time the function is done running.
var lastActiveTool;
on(dom.byId("div-tools-draw"), "click", function (evt) {
if(typeof lastActiveTool !== 'undefined'){
//Do what you need to do with the last id. Add an else if you want something special to happen when the first element is clicked and there is no previous id.
}
lastActiveTool = evt.target.id;
}

jQuery bind custom function to appended element

I am trying to create a web app that will allow a user to define a custom JavaScript function and then add a button to their user interface that well preform that function.
Here is a sample of the code
var customCommands = {
command1: {
text: 'Hello Console',
cFunctionRun: function() {
console.log('hello Console!');
}
},
command2: {
text: 'Hello World',
cFunctionRun: function() {
alert('hello World!');
}
}
}
Then I wrote a small function that loops though and builds the buttons and adds them to the user interface. The problem is when I append the elements to the user interface than click on the buttons nothing works...
Here is one of the methods I tried
for (var cmd in customCommands) {
command = customCommands[cmd];
button = $('<button/>').html(command.text).on('click',
function(){
console.log(command.text);
command.cFunctionRun();
}
);
}
buttonContainer.append(button);
Now my loop builds everything just fine and even the .on('click') works, but it always displays the text of the lasted added command?
here is http://jsfiddle.net/nbnEg/ to show what happens.
When you actually click, the command variable points to last command (as the whole loop has already run). You should maintain data state per button which tells it which command to invoke. You should do this.
for(var i in customCommands) {
if(customCommands.hasOwnProperty(i)){ //this is a pretty important check
var command = customCommands[i];
button = $('<button/>').html(command.text).data("command_name", command).on('click', function(){
console.log($(this).data("command_name").text);
$(this).data("command_name").cFunctionRun();
});
$("body").append(button);
}
}
JSFiddle
all you need is passing the parameter with function, you should try this
It's a (missing) closure problem. The event handler will keep a reference to the value of command on the last iteration of the loop. To solve it you can create a new scope, using an immediately invoked function:
for(var cmd in customCommands) {
(function(command){
button = $('<button/>').html(command.text).on('click',
function(){
console.log(command.text);
command.cFunctionRun();
}
);
buttonContainer.append(button);
}(customCommands[cmd]));
}
Since the buttons should be unique (no reason for creating duplicates), I'm setting the button id to the name of the customCommands (command1 and command2 in this example). This example could easily be adapted to use any of the relative attributes (data-*, name, etc...).
Create a click event listener on document for whenever one of your buttons are pressed. Then call the function associated with the given id.
$(document).on("click", "button", function(){
customCommands[this.id].cFunctionRun();
});
for(var command in customCommands){
var button = $('<button id="' + command +'"/>').html(customCommands[command].text);
$("body").append(button);
}
EXAMPLE

jQuery Event Handler created in loop

So I have a group of events like this:
$('#slider-1').click(function(event){
switchBanners(1, true);
});
$('#slider-2').click(function(event){
switchBanners(2, true);
});
$('#slider-3').click(function(event){
switchBanners(3, true);
});
$('#slider-4').click(function(event){
switchBanners(4, true);
});
$('#slider-5').click(function(event){
switchBanners(5, true);
});
And I wanted to run them through a loop I am already running something like this:
for(i = 1; i <= totalBanners; i++){
$('#slider-' + i).click(function(event){
switchBanners(i, true);
});
}
In theory that should work, but it doesnt seem to once I load the document... It doesnt respond to any specific div id like it should when clicked... it progresses through each div regardless of which one I click. There are more event listeners I want to dynamically create on the fly but I need these first...
What am I missing?
This is a very common issue people encounter.
JavaScript doesn't have block scope, just function scope. So each function you create in the loop is being created in the same variable environment, and as such they're all referencing the same i variable.
To scope a variable in a new variable environment, you need to invoke a function that has a variable (or function parameter) that references the value you want to retain.
In the code below, we reference it with the function parameter j.
// Invoke generate_handler() during the loop. It will return a function that
// has access to its vars/params.
function generate_handler( j ) {
return function(event) {
switchBanners(j, true);
};
}
for(var i = 1; i <= totalBanners; i++){
$('#slider-' + i).click( generate_handler( i ) );
}
Here we invoked the generate_handler() function, passed in i, and had generate_handler() return a function that references the local variable (named j in the function, though you could name it i as well).
The variable environment of the returned function will exist as long as the function exists, so it will continue to have reference to any variables that existed in the environment when/where it was created.
UPDATE: Added var before i to be sure it is declared properly.
Instead of doing something this .. emm .. reckless, you should attach a single event listener and catch events us they bubble up. Its called "event delegation".
Some links:
http://davidwalsh.name/event-delegate
http://net.tutsplus.com/tutorials/javascript-ajax/quick-tip-javascript-event-delegation-in-4-minutes/
http://www.sitepoint.com/javascript-event-delegation-is-easier-than-you-think/
http://lab.distilldesign.com/event-delegation/
Study this. It is a quite important thing to learn about event management in javascript.
[edit: saw this answer get an upvote and recognized it's using old syntax. Here's some updated syntax, using jQuery's "on" event binding method. The same principle applies. You bind to the closest non-destroyed parent, listening for clicks ON the specified selector.]
$(function() {
$('.someAncestor').on('click', '.slider', function(e) {
// code to do stuff on clicking the slider. 'e' passed in is the event
});
});
Note: if your chain of initialization already has an appropriate spot to insert the listener (ie. you already have a document ready or onload function) you don't need to wrap it in this sample's $(function(){}) method. You would just put the $('.someAncestor')... part at that appropriate spot.
Original answer maintained for more thorough explanation and legacy sample code:
I'm with tereško : delegating events is more powerful than doing each click "on demand" as it were. Easiest way to access the whole group of slider elements is to give each a shared class. Let's say, "slider" Then you can delegate a universal event to all ".slider" elements:
$(function() {
$('body').delegate('.slider', 'click', function() {
var sliderSplit = this.id.split('-'); // split the string at the hyphen
switchBanners(parseInt(sliderSplit[1]), true); // after the split, the number is found in index 1
});
});
Liddle Fiddle: http://jsfiddle.net/2KrEk/
I'm delegating to "body" only because I don't know your HTML structure. Ideally you will delegate to the closest parent of all sliders that you know is not going to be destroyed by other DOM manipulations. Often ome sort of wrapper or container div.
It's because i isn't evaluated until the click function is called, by which time the loop has finished running and i is at it's max (or worse overwritten somewhere else in code).
Try this:
for(i = 1; i <= totalBanners; i++){
$('#slider-' + i).click(function(event){
switchBanners($(this).attr('id').replace('slider-', ''), true);
});
}
That way you're getting the number from the id of the element that's actually been clicked.
Use jQuery $.each
$.each(bannersArray, function(index, element) {
index += 1; // start from 0
$('#slider-' + index).click(function(event){
switchBanners(index, true);
});
});
You can study JavaScript Clousure, hope it helps

How to refer to object in JavaScript event handler?

Note: This question uses jQuery but the question has nothing to do with jQuery!
Okay so I have this object:
var box = new BigBox();
This object has a method named Serialize():
box.AddToPage();
Here is the method AddToPage():
function AddToPage()
{
$('#some_item').html("<div id='box' onclick='this.OnClick()'></div>");
}
The problem above is the this.OnClick() (which obviously does not work). I need the onclick handler to invoke a member of the BigBox class. How can I do this?
How can an object refer to itself in an event handler?
You should attach the handler using jQuery:
function AddToPage()
{
var self = this;
$('#some_item').empty().append(
$("<div id='box'></div>")
.click(function() { self.OnClick(someParameter); })
);
}
In order to force the event handler to be called on the context of your object (and to pass parameters), you need to add an anonymous function that calls the handler correctly. Otherwise, the this keyword in the handler will refer to the DOM element.
Don't add event handlers with inline code.
function AddToPage()
{
$('#some_item').html("<div id='box'></div>");
$('#box').click(this.OnClick);
}
EDIT:
Another way (avoids the extra select):
function AddToPage()
{
var div = $('<div id="box"></div>'); // probably don't need ID anymore..
div.click(this.OnClick);
$('#some_item').append(div);
}
EDIT (in response to "how to pass parameters");
I'm not sure what params you want to pass, but..
function AddToPage()
{
var self = this, div = $('<div></div>');
div.click(function (eventObj) {
self.OnClick(eventObj, your, params, here);
});
$('#some_item').append(div);
}
In jQuery 1.4 you could use a proxy.
BigBox.prototype.AddToPage= function () {
var div= $('<div>', {id: box});
div.click(jQuery.proxy(this, 'OnClick');
div.appendTo('#some_item');
}
You can also use a manual closure:
var that= this;
div.click(function(event) { that.OnClick(event); });
Or, most simply of all, but requiring some help to implement in browsers that don't yet support it (it's an ECMAScript Fifth Edition feature):
div.click(this.OnClick.bind(this));
If you are using jQuery, then you can separate your code from your markup (the old seperation of concerns thing) like this
$(document).ready(function() {
var box = new BigBox();
$('#box').click(function() {
box.serialize();
});
});
You only need to add the click handler once for all divs with id of box. And because the click is an anonymous function, it gets the scope of the function it is placed in and therefore access to the box instance.

Categories