JavaScript closures and variable scope - javascript

I am having trouble with JS closures:
// arg: an array of strings. each string is a mentioned user.
// fills in the list of mentioned users. Click on a mentioned user's name causes the page to load that user's info.
function fillInMentioned(mentions) {
var mentionList = document.getElementById("mention-list");
mentionList.innerHTML = "";
for (var i = 0; i < mentions.length; i++) {
var newAnchor = document.createElement("a");
// cause the page to load info for this screen name
newAnchor.onclick = function () { loadUsernameInfo(mentions[i]) };
// give this anchor the necessary content
newAnchor.innerHTML = mentions[i];
var newListItem = document.createElement("li");
newListItem.appendChild(newAnchor);
mentionList.appendChild(newListItem);
}
document.getElementById("mentions").setAttribute("class", ""); // unhide. hacky hack hack.
}
Unfortunately, clicking on one of these anchor tags results in a call like this:
loadUserNameInfo(undefined);
Why is this? My goal is an anchor like this:
<a onclick="loadUserNameInfo(someguy)">someguy</a>
How can I produce this?
Update This works:
newAnchor.onclick = function () { loadUsernameInfo(this.innerHTML) };
newAnchor.innerHTML = mentions[i];

The "i" reference inside the closure for the onclick handlers is trapping a live reference to "i". It gets updated for every loop, which affects all the closures created so far as well. When your while loop ends, "i" is just past the end of the mentions array, so mentions[i] == undefined for all of them.
Do this:
newAnchor.onclick = (function(idx) {
return function () { loadUsernameInfo(mentions[idx]) };
})(i);
to force the "i" to lock into a value idx inside the closure.

Your iterator i is stored as a reference, not as a value and so, as it is changed outside the closure, all the references to it are changing.
try this
function fillInMentioned(mentions) {
var mentionList = document.getElementById("mention-list");
mentionList.innerHTML = "";
for (var i = 0; i < mentions.length; i++) {
var newAnchor = document.createElement("a");
// Set the index as a property of the object
newAnchor.idx = i;
newAnchor.onclick = function () {
// Now use the property of the current object
loadUsernameInfo(mentions[this.idx])
};
// give this anchor the necessary content
newAnchor.innerHTML = mentions[i];
var newListItem = document.createElement("li");
newListItem.appendChild(newAnchor);
mentionList.appendChild(newListItem);
}
document.getElementById("mentions").setAttribute("class", "");
}

Related

Javascript. Onclick returns always the same object

I have got 16 divs of the same class name in html document in the following fashion
<div class="game-selection-tab">
<h2>30m + 15s</h2>
</div>
<div class="game-selection-tab">
<h2>60m + 0s</h2>
</div>
<div class="game-selection-tab">
<h2>Custom</h2>
</div>
I want to create onclick method that returns h2 text content from particular div. I tried to solve this by using following javascript code.
var selectionTabs = document.getElementsByClassName("game-selection-tab");
for(var i = 0; i < selectionTabs.length; i++){
var tab = selectionTabs[i];
var content = tab.getElementsByTagName("h2");
tab.onclick = function(){
console.log(content[0].textContent);
}
}
The problem is: no matter which div i click, program always returns h2 text content from the last div(in this example "custom").
Try this
var selectionTabs = document.getElementsByClassName("game-selection-tab");
for(var i = 0; i < selectionTabs.length; i++){
(function (index) {
var tab = selectionTabs[index];
var content = tab.getElementsByTagName("h2");
tab.onclick = function(){
console.log(content[0].textContent);
}
})(i);
}
The thing is by the time your event attaches to the actual DOM element the for loop execution is complete and the value of i is the max value that it can reach. Hence, isolating the same in a function like this works. The function stores the value of i or in this case index as the original value that you expect.
Replace
var i = 0
by
let i = 0
and you're done.
A detailed explanation is here.
I'll quote my answer for your understanding below.
Cause of the problem: lack of understanding scope
Check this example to understand the problem:
var creates function scope
var funcs = []
for (var i = 0; i < 10; i++) {
funcs.push(function() {
console.log(i)
})
}
funcs.forEach(function(func) {
func()
})
While you might expect this forEach loop to result in number 0 to 9 being printed, instead you get ten times 10. The cause of this is the variable i being declared using var keyword, which creates a function scope that leads to each function in funcs holding a reference to the same i variable. At the time the forEach loop is executed, the previous for-loop has ended and i holds 10 (9++ from the last iteration).
Compare how ES6's let, which creates block scope instead of function scope, behaves in this regard:
let (ES6 or officially ES2015) creates block scope:
var funcs = []
for (let i = 0; i < 10; i++) {
funcs.push(function() {
console.log(i)
})
}
funcs.forEach(function(func) {
func()
})
Because let creates block scope, each iteration of the for loop has its "own" variable i.
ES5 solution using an IIFE wrapper
If you need an ES5 solution, an IIFE (immediately invoked function expression) wrapper would be the way to go:
var funcs = []
for (var i = 0; i < 10; i++) {
funcs.push((function(value) {
return function() {
console.log(value)
}
}(i)))
}
funcs.forEach(function(func) {
func()
})
Here, i is passed as a parameter to each function which stores its own copy value.
The same is true for for..in loops:
var funcs = [],
obj = {
first: "first",
last: "last",
always: "always"
}
for (var key in obj) {
funcs.push(function() {
console.log(key)
})
}
funcs.forEach(function(func) { // outputs: "always", "always", "always"
func()
})
Again, all functions in funcs hold the reference to the same key because var key creates a function scope that lives outside of the for..in loop. And again, let produces the result you'd probably rather expect:
var funcs = [],
obj = {
first: "first",
last: "last",
always: "always"
}
for (let key in obj) {
funcs.push(function() {
console.log(key)
})
}
funcs.forEach(function(func) {
func()
})
Also compare the excellent (!) book
Nicholas C. Zakas: "Understanding ES6", no starch press, p. 8-9.
from which the examples were taken.
It's showing always the same value because you are setting content outside of the onclick function. After the for loop, content points to the last h2.
Move the content definition inside the onclick function.
tab.onclick = function(){
var content = this.getElementsByTagName("h2");
console.log(content[0].textContent);
}
Working fiddle
Can you try the solution below.
var selectionTabs = document.getElementsByClassName("game-selection-tab");
Object.keys(selectionTabs).forEach((data, index) => {
var context = selectionTabs[data].getElementsByTagName("h2")[0].textContent;
selectionTabs[data].onclick = function () {
console.log(context)
}
})
Try this simple solution:
var els = document.getElementsByClassName('game-selection-tab');
var index = 0;
function getText() {
alert(this.innerText || this.textContent);
}
for (; index < els.length; index++) {
els[index].onclick = getText;
}
<div class="game-selection-tab">
<h2>30m + 15s</h2>
</div>
<div class="game-selection-tab">
<h2>60m + 0s</h2>
</div>
<div class="game-selection-tab">
<h2>Custom</h2>
</div>
Assuming that you don't wanna change your HTML to include an "onclick" event on each h2, this code might help you:
document.addEventListener('click', function(e) {
e = e || window.event;
var target = e.target || e.srcElement,
text = target.textContent || text.innerText;
console.log(text);
}, false);
EDIT
If you want to be more specific and get the content from only your h2's, you could use this:
h2s = document.getElementsByTagName('h2');
for (var i = 0; i < h2s.length; i++) {
h2s[i].addEventListener('click', redirect, false);
}
function redirect(e) {
var target = e.target || e.srcElement;
var text = target.textContent || text.innerText;
console.log(text);
}

Do I need a parameter in this function?

In this homework assignment, I'm having issues with this part of the problem.
window.onload=setup;
function setup()
{
var questions = document.querySelectorAll('ol li');
for (var i= 0; i < questions.length ; i++)
{
questions[i].id = i + "phrases";
questions[i].onmousedown = showEnglish;
//questions[i].onmouseup = showFrench;
questions[i].style.cursor = "pointer";
}
}
function showEnglish()
{
var phraseNumber = parseInt(question[i].id)
document.getElementById(phraseNumber).innerHTML = english[phraseNumber];
english[phraseNumber].style.font = "italic";
english[phraseNumber].style.Color = "rgb(191,22,31)";
}
a) Using the id property of the list item experiencing the mousedown event, extract the index number with the the parseInt() function and store that value in the phraseNumber variable.
I get an error, saying questions is not defined in the showenglish().
Am I supposed to be referencing another object?
You need to pass the question as a parameter:
for(i=0;i<question.length;i++){
let a=i;//important for scoping
question[a].onmousedown=function(){
showEnglish(question[a]);
}
}
function showEnglish(question){
document.getElementById(question.id).style.font="italic";
...
}
(Note: this answer contains ES6. Do not use it in real productional environment. The let a=i; defines that a is kept for being used inside of the listener, while i will always be question.length, because the event is probably clicked after the loop occured...)
Alternatively, the event listener binds this as the clicked element:
question[i].addEventListener("click",showEnglish,false);
function showEnglish(){
document.getElementById(this.id).style.font="italic";
...
}
The mousedown event is raised when the user presses the mouse button. Look at the documentation for the mousedown event.
Your event handler function will be passed an Event object, which has a target property, which is a reference to the element that the mouse clicked on.
You can access this inside your event handler function with event.target.
window.onload = setup;
function setup() {
var questions = document.querySelectorAll('ol li');
for (var i = 0; i < questions.length; i++) {
questions[i].id = i + "phrases";
questions[i].onmousedown = showEnglish;
//questions[i].onmouseup = showFrench;
questions[i].style.cursor = "pointer";
}
}
function showEnglish(event) {
var phraseNumber = parseInt(event.target.id);
// etc
};

Javascript reference in loop: "Uncaught TypeError: Cannot read property 'value' of undefined"

I tried debugging my code for like a few hour but I got nothing out of it. The issue is that it makes absolutely no sense on why it reports an error every time I tried to use document.forms[0][i] (i as the iterator) in the event listener but "this" satisfies the code.
//broken
var addListeners = function() {
var i;
var formFields = document.forms[0];
var formSubmit = formFields["submit"];
for (i = 0; i < formFields.length; i++) {
if (formFields[i] != formSubmit) {
formFields[i].onblur = (function () {
checkNonEmpty(formFields[i]);
});
}
}
};
//works
var addListeners = function() {
var i;
var formFields = document.forms[0];
var formSubmit = formFields["submit"];
for (i = 0; i < formFields.length; i++) {
if (formFields[i] != formSubmit) {
formFields[i].onblur = (function () {
checkNonEmpty(this);
});
}
}
};
Wouldn't "this" refer to document.forms[0][i]?... formFields references to document.forms[0]. However the exact same code (with "this" where formFields[i] is at) works just fine.
Here is the demo: http://jsfiddle.net/PbHwy/
Cranio's answer already contains the root of the matter. To get rid of this you can either include formFields[i] by using closures
var blurCallbackGenerator = function(element){
return function () {
checkNonEmpty(element);
};
};
formFields[i].onblur = blurCallbackGenerator(formFields[i]);
/* // dense version:
formFields[i].onblur = (function(element){
return function () {
checkNonEmpty(element);
};
})(formFields[i]);
*/
or simply using this.
See also:
MDN: Creating closures in loops: A common mistake
Because you define formFields in a scope outside (or better, different than) the event listener. When the event listener is called, it is called not in the addListeners function where you define formFields, but "independently", so the reference is lost and its value is undefined (but this works because it is not dependent on that scope).
The problem is that the variable i (referred to in each of your handlers) is the exact same variable in each of them, which by the time the loop has finished has value formFields.length+1 and is therefore wrong for all of them. Try this instead [note: the below used to say something VERY WRONG before I edited it -- thanks to Zeta for pointing out my mistake]:
var addListeners = function() {
var i;
var formFields = document.forms[0];
var formSubmit = formFields["submit"];
for (i = 0; i < formFields.length; i++) {
if (formFields[i] != formSubmit) {
formFields[i].onblur = (function(j) {
return (function () {
checkNonEmpty(formFields[j]);
})(i);
});
}
}
};
and you'll find it works (unless there's another bug that I haven't noticed).
If you can afford to support only Javascript 1.7 and above, you can instead write your old code but make your for look like this: for (let i=0; i<formFields.length; i++). But you quite possibly can't.

For loop inside function (newbie)

I would like to have some variables that my for loop uses inside a function scope (not global).
I tried to wrap the for loop inside a function like this but it results in console error:
function() {
var data = livingroomTableData;
for(var i = data[0]; i < data[1]; i++) {
var elemvalue = data[2] + format(i) + ".png";
livingroomTableArray[i] = elemvalue;
}
}
I would like the data variable to have the values of livingroomTableData only inside this for loop (not globally). In other loops I will input a different variable into the data variable.
Oh yes, and as you can probably tell, I'm a total newbie. :S
There is only function scope in javascript, block scope does not exist, so you can't let the variable only inside the for loop. What you could do is to create a function scope.
Code example:
(function(livingroomTableData) {
var data = livingroomTableData;
//... the rest code
})(livingroomTableData);
The big problem is this line:
for(var i = data[0]; i < data[1]; i++) {
That means, starting with i as the first element of the array, do the code in the loop, incrementing i by one at the end of each run until i is not less than the second element of data.
I'd rewrite it to show you a working version, but its not clear what you actually want to do.
function() {
for(var i = 0; i < livingroomTableData.length; i++) {
var data = livingroomTableData[i];
//your code here...
}
}

Speed test: while() cycle vs. jQuery's each() function

I have an array of objects called targets and I want to execute a function on each of those objects. The first method:
targets.each(function() {
if (needScrollbars($(this))) {
wrap($(this), id);
id = id + 1;
}
});
This method gives execution speed of ~125ms. The second method is:
var i=0;
while (targets[i] != undefined) {
if (needScrollbars($(this))) {
wrap($(this), id);
id = id + 1;
}
i = i+1;
}
This second method takes whopping 1385ms to execute and I get my head around that. Does anyone have any idea why a bare bones cycle runs slower than a function which I'm only guessing that's doing (just guessing) a whole lot more than a simple cycle?
Thank you.
They are totally different. The this in the first example is the current target, in the second example this is the "external" this. You should change the second example as:
var i=0;
while (targets[i] != undefined) {
var cur = $(targets[i]);
if (needScrollbars(cur)) {
wrap(cur, id);
id = id + 1;
}
i = i+1;
}
The relevant quote
More importantly, the callback is fired in the context of the current DOM element, so the keyword this refers to the element.
But I don't know why you haven't written as:
for (var i = 0; i < targets.length; i++)
{
var cur = $(targets[i]);
if (needScrollbars(cur)) {
wrap(cur, id);
id = id + 1;
}
}
And in the end the each "method" is easier to comprehend (for me).
Your second method is not functionally equivalent to the first one.
Why? Because it uses this, making it a closure on the global scope. Of course the second method is slower: it continuously shells out jQuery objects made out of global scope. Try that benchmark again with:
var i=0;
while (targets[i] !== undefined) {
var o = $(targets[i]);
if (needScrollbars(o)) {
wrap(o, id);
id++;
}
i++;
}

Categories