This question already has answers here:
JavaScript closure inside loops – simple practical example
(44 answers)
Closed 6 years ago.
Can someone tell me what I am doing wrong? I've spent an entire day troubleshooting this but I am getting nowhere... I want to add the event "onmouseover" to my span elements. However when I implement the code below, nothing happens. I did a bit of googling and I think it might be a variable scope problem?? Im not too sure... Any help is appreciated!
<!DOCTYPE html>
<html>
<head>
<title>Fixing bugs in JS</title>
<script src="question1.js" type="text/javascript"></script>
<head>
<body>
<div id="output"></div>
</body>
<html>
var NUMBERS = 100;
function go()
{
var out = document.getElementById("output");
for (var i = 1; i < NUMBERS+1; i++) {
var span_one = document.createElement("span");
span_one.id = "span" + i;
span_one.innerHTML = "" + i;
out.appendChild(span_one);
if (isPrime(i) === true) { // where i is a prime number (3, 5, 7..etc)
span_one.style.backgroundColor = "red";
span_one.onmouseover = function() {
hover("span"+i, "yellow", "150%")
};
span_one.onmouseout = function() {
hover("span"+i, "red", "100%") // whatever color in this line always overrides previous set color...
};
}
function hover(id, color, size) {
var span = document.getElementById(id);
span.style.backgroundColor = color;
span.style.fontSize = size;
}
function etc() {
...
}
window.onload=go;
There's really no need to (a) give the elements an id (b) to use the i counter for anything other than the loop of creating them.
Here's an alternative.
function newEl(tag){return document.createElement(tag)}
function byId(id){return document.getElementById(id)}
window.addEventListener('load', onDocLoaded, false);
function onDocLoaded(evt)
{
var i, n = 100;
var outputContainer = byId('output');
for (i=1; i<=n; i++)
{
var span = newEl('span');
//span.id = 'span_' + i;
span.textContent = i;
outputContainer.appendChild(span);
if ( i%2 == 1) // isOdd
{
span.addEventListener('mouseover', onSpanMouseOver, false);
span.addEventListener('mouseout', onSpanMouseOut, false);
}
}
}
function onSpanMouseOver(evt)
{
this.style.backgroundColor = 'yellow';
this.style.fontSize = '150%';
}
function onSpanMouseOut(evt)
{
this.style.backgroundColor = 'red';
this.style.fontSize = '100%';
}
<div id='output'></div>
Your issue is that you have closures around your i variable.
Closures occur whenever you nest a function within another function. Where the code runs unpredictably is when the nested function uses a variable from an ancestor function and the nested function has a longer lifetime than the ancestor in question.
Here, your mouseover and mouseout functions rely on i from the parent function go. Since the mouseover and mouseout functions are being attached to DOM elements and those DOM elements are going to remain in memory until the page is unloaded, those functions will have a longer lifetime than go. This means that the i variable that go declared can't go out of scope when go completes and that both of the mouse functions will SHARE the same value of i. The value that i has by the time a human comes along and moves the mouse is the LAST value it had when the loop ended.
Closures can be challenging at first, but you can read a bit more about them here.
Changing var i to let i on your loop solves that because let introduces block scope for each iteration of the loop.
Also, I saw that you were missing two closing curly braces that were causing errors. I added my own isPrime() function. See comments for locations:
window.onload=go;
const NUMBERS = 100;
function go(){
var out = document.getElementById("output");
// Using let instead of var avoids a closure by making sure
// that each looping number exists in the block scope of the
// loop and upon each iteration a new variable is created.
for (let i = 1; i < NUMBERS+1; i++) {
var span = document.createElement("span");
span.id = "span" + i;
span.innerHTML = i + "<br>";
out.appendChild(span);
if (isPrime(i)) { // where i is a prime number (2, 3, 5, 7..etc)
span.style.backgroundColor = "red";
// If you use the i variable in nested functions, you will create a
// closure around it and both the mouseover and mouseout functions will
// share the last known value of i. Each function must get its own copy
// of i.
span.onmouseover = function() {
hover("span" + i, "yellow", "150%")
};
span.onmouseout = function() {
// whatever color in this line always overrides previous set color...
hover("span" + i, "red", "100%")
};
} // <-- Missing
} // <-- Missing
}
function isPrime(value) {
for(var i = 2; i < value; i++) {
if(value % i === 0) {
return false;
}
}
return value > 1;
}
function hover(id, color, size) {
var span = document.getElementById(id);
span.style.backgroundColor = color;
span.style.fontSize = size;
console.log(id, span);
}
<div id="output"></div>
Here's a working example:
http://jsbin.com/zixeno/edit?js,console,output
The problem is exactly what enhzflep said. One solution is to to move the "addSpan" logic out of the for loop and into a function.
var NUMBERS = 100;
function go() {
var out = document.getElementById("output");
for (var i = 1; i < NUMBERS+1; i++) {
addSpan(i);
}
function hover(id, color, size) {
var span = document.getElementById(id);
span.style.backgroundColor = color;
span.style.fontSize = size;
}
function addSpan(i) {
var span_one = document.createElement("span");
span_one.id = "span" + i;
span_one.innerHTML = "" + i;
out.appendChild(span_one);
if (isPrime(i) === true) {
span_one.style.backgroundColor = "red";
span_one.onmouseover = function() {
hover("span"+i, "yellow", "150%")
};
span_one.onmouseout = function() {
hover("span"+i, "red", "100%");
};
}
}
}
The problem is with the variable i, its a common issue with closures. For more information you can have a look at MDN closures and go to the section Creating closures in loops: A common mistake. To come over this issue, change the var in for loop to let. This will help you retain the scope and thus fixing the issue.
var NUMBERS = 100;
function go() {
var out = document.getElementById("output");
for (let i = 1; i < NUMBERS+1; i++) {
let span_one = document.createElement("span");
span_one.id = "span" + i;
span_one.innerHTML = "" + i;
out.appendChild(span_one);
if (isPrime(i) === true) { // if a number is a prime then run this func
span_one.style.backgroundColor = "red";
span_one.onmouseover = function() {
hover("span"+i, "yellow", "150%")
};
span_one.onmouseout = function() {
hover("span"+i, "red", "100%") // whatever color in this line always overrides previous set color...
};
}
function hover(id, color, size) {
var span = document.getElementById(id);
span.style.backgroundColor = color;
span.style.fontSize = size;
}
//Added my custom function as it was not provided
function isPrime(i){
return i%2 != 0;
}
}
}
window.onload = go;
<div id="output"></div>
Related
var change = function() {
var elem = document.getElementsByTagName("body");
var count = 0;
count++;
var color = "";
var colors = ["#ff6051", "#ff9f51", "#ffdf51", "#b6ff51", "#51adff", "#3e65c1", "#6414ef"];
for (var i = 0; i < colors.length; i++) {
if (count == i + 1) {
color = colors[i];
}
}
elem[0].style.backgroundColor = color;
}
<button onclick="change()">Click me</button>
I want the background color of the body to change when I click the button.
But the number of variable "count" doesn't seem to increase. What should I do to make the number increase?
Declare the variabe count outside the function so that it gets the global scope whenever you update.
DEMO
var count = 0;
var change = function() {
var elem = document.getElementsByTagName("body");
count++;
console.log('##count',count);
var color = "";
var colors = ["#ff6051", "#ff9f51", "#ffdf51", "#b6ff51", "#51adff", "#3e65c1", "#6414ef"];
for (var i = 0; i < colors.length; i++) {
if (count == i + 1) {
color = colors[i];
}
}
elem[0].style.backgroundColor = color;
}
<button onclick="change()">Click me</button>
Without changing your code too much, you can avoid the loop and iterate over the array by comparing the current color. This also avoids holding a count iterator.
Note:
Some browsers return backgroundColor as an rgb value (e.g., rgb( ###, ###, ###)), which is why rgb2hex is used to convert the it to the hex value like that stored in the colors array.
var change = function() {
var el = document.querySelector("body");
var colors = ["#ff6051", "#ff9f51", "#ffdf51", "#b6ff51", "#51adff", "#3e65c1", "#6414ef"];
var currentColor = rgb2hex( el.style.backgroundColor );
var colorIndex = colors.indexOf( currentColor );
// If at last color, cycle back to front
if (colorIndex == colors.length-1)
colorIndex = -1;
el.style.backgroundColor = colors[colorIndex + 1];
}
/** Converts decimal to hex **/
function hex(x) {
return ("0" + parseInt(x).toString(16)).slice(-2);
}
/** Converts rgb string to hex string **/
function rgb2hex(rgb) {
if (rgb.search("rgb") == -1)
return rgb;
else {
rgb = rgb.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+))?\)$/);
return "#" + hex(rgb[1]) + hex(rgb[2]) + hex(rgb[3]);
}
}
<button onclick="change()">Click me</button>
Each call to your function is resetting count to 0 because you are setting it to zero on the second line of the function.
If you set it to 0 outside the function once, this will solve the count problem.
However, there is an additional problem (that you didn't mention): after counting to 7, you run out of colours in your array because count exceeds the bounds of the array. I would lose the for loop since it is unnecessary (use count to index into the array instead) and just reset count when it reaches the size of the array.
var count = 0;
var colors = ["#ff6051", "#ff9f51", "#ffdf51", "#b6ff51", "#51adff", "#3e65c1", "#6414ef"];
var change = function() {
var elem = document.getElementsByTagName("body");
if (count == colors.length) {
count = 0;
}
elem[0].style.backgroundColor = colors[count];
count++;
}
<button onclick="change()">Click me</button>
The count variable has local scope, so it will not exist after the anonymous function expression referred by change variable finishes execution. For it to sustain its life time across repeated function calls on button click action it should be declared in global scope outside the anonymous function expression:
var count = 0; //now count has global scope.
var change = function() {
var elem = document.getElementsByTagName("body");
count++;
var color = "";
var colors = ["#ff6051", "#ff9f51", "#ffdf51", "#b6ff51", "#51adff", "#3e65c1", "#6414ef"];
for (var i = 0; i < colors.length; i++) {
if (count == i + 1) {
color = colors[i];
}
}
elem[0].style.backgroundColor = color;
}
<button onclick="change()">Click me</button>
Note: You should also consider giving some default color inside the for loop as after 7 clicks it will be setting the backgroundColor to empty string.
var color = "#000000"; //default black color may be
To achieve expected result, use below option
No need of for loop
After 7 clicks , loop runs again
One issue with your code is, first background color will always be skipped due i+1 and after 7 clicks , it comes back to white background
var count = 0;
var colors = ["#ff6051", "#ff9f51", "#ffdf51", "#b6ff51", "#51adff", "#3e65c1", "#6414ef"];
var change = function() {
if(count == colors.length + 1){
count =0;
}
++count;
var elem = document.getElementsByTagName("body");
elem[0].style.backgroundColor = colors[count];
}
https://codepen.io/divyar34/pen/EbZxPm
You can generically count the invocations of any function by "lifting" the function (wrapping it) with a function that just does that.
This leaves you with a simple function that doesn't need to know if it is being counted, which you can wrap with a counter only when you need it.
function countingWrapper(f,reportf)
{
var counter = 0;
return function()
{
reportf( ++counter)
return f.apply(this, arguments);
}
}
function doSomething() { console.log('boo!'); }
function reportSomething(n) { console.log('did something '+ n + ' times.')}
var newDoSomething = countingWrapper(doSomething, reportSomething);
newDoSomething();
newDoSomething();
newSoSomething();
I have a nav bar where each button changes the background of the body. They each change it to a different color. I have created onmouseover and onmouseout functions for each button to achieve this. However, I wonder if there is a way to just write one of each function by just referring to them by their class? They all have the same class of button. Is there a way a function can apply to all elements of a certain class? My code:
function whichButton(x) {
if (x==1)
return "red";
if (x==2)
return "green";
if (x==3)
return "blue";
if (x==4)
return "orange";
if (x==0)
return initBG;
}
button1.onmouseover = function() {
document.body.style.backgroundColor = whichButton(1);
}
button1.onmouseout = function() {
document.body.style.backgroundColor = whichButton(0);
}
button2.onmouseover = function() {
document.body.style.backgroundColor = whichButton(2);
}
button2.onmouseout = function() {
document.body.style.backgroundColor = whichButton(0);
}
button3.onmouseover = function() {
document.body.style.backgroundColor = whichButton(3);
}
button3.onmouseout = function() {
document.body.style.backgroundColor = whichButton(0);
}
button4.onmouseover = function() {
document.body.style.backgroundColor = whichButton(4);
}
button4.onmouseout = function() {
document.body.style.backgroundColor = whichButton(0);
}
initBG just saves the initial background of the page.
I have tried this:
document.getElementsByClassName('button').onmouseover = function() {
document.body.style.backgroundColor = whichButton(1);
}
but it doesn't trigger the function. And I guess to do this, I'd also need to have a way to read the elements' ID as a string so I could get it's number...
This is more out of curiosity than necessity, just trying to find ways to keep my code small! I could see this being useful in many applications so I'd love to learn more about this!
Corresponding HTML:
<div id="navbar">
<p id="button1" class="button">Red</p><p id="button2" class="button">Blue</p><p id="button3" class="button">Green</p><p id="button4" class="button">Orange</p>
</div>
Here is my suggestion to solve it:
Use the data attribute and iterate over all elements with the given class.
function applyColor(element) {
var color = element.getAttribute('data-bg');
document.body.style.backgroundColor = color;
}
var buttons = document.getElementsByClassName("button");
for (var i = 0; i < buttons.length; i++) {
buttons[i].addEventListener("mouseover", function() {
applyColor(this);
}, false);
}
<nav>
<button class="button" data-bg="red">red</button>
<button class="button" data-bg="blue">blue</button>
<button class="button" data-bg="yellow">yellow</button>
<button class="button" data-bg="green">green</button>
<button class="button" data-bg="pink">pink</button>
<button class="button" data-bg="magenta">magenta</button>
</nav>
As previously stated, getElementsByClassName returns a collection and you can't just add the event to the collection in a way that you can in jQuery. To do this is pure JS, you need to use a for loop and then attach the event to each individual element as below:
var buttons = document.getElementsByClassName('button');
for (var i = 0; i < buttons.length; i++) {
buttons[i].onmouseover = function (event) {
var colour = event.target.className.split(" ")[1];
document.body.style.backgroundColor = colour;
}
}
http://jsfiddle.net/andyfurniss/1n5vann9/
getElementsByClassName returns a collection. So you will have to loop over it and you shall be good.
var buttons = document.getElementsByClassName('button');
[].forEach.call(buttons, function (button){
var id = parseInt(button.id.split("").reverse().join("")); //This will give you the number from the id
button.onmouseover = = function() {
document.body.style.backgroundColor = whichButton(id);
}
button.onmouseout = function() {
document.body.style.backgroundColor = whichButton(0);
}
});
To ensure ES6 compatibility, there is much better way.
var buttons = document.getElementsByClassName("button");
for (button of buttons) {
var id = parseInt(button.id.split("").reverse().join("")); //This will give you the number from the id
button.onmouseover = = function() {
document.body.style.backgroundColor = whichButton(id);
}
button.onmouseout = function() {
document.body.style.backgroundColor = whichButton(0);
}
}
The first comment actually solved it for me. I did this:
document.onmouseover = function() {
var x = event.target;
y = x.id.toString().replace('button','');
if (y > 0 && y <= 4)
document.body.style.backgroundColor = whichButton(y);
}
document.onmouseout = function() {
var x = event.target;
y = x.id.toString().replace('button','');
if (y > 0 && y <= 4)
document.body.style.backgroundColor = whichButton(0);
}
If I mouse over a "button", it removes the word "button", leaving the number (1-4), then sends that to my whichButton function to decice which colour to use. Nice and simple, works for me.
You can use event delegation, which means attaching an event listener to an ancestor, then inspecting the event.target to decide what to do.
Demo: http://jsfiddle.net/a58tj1ak/
// given your HTML and whichButton function like this:
function whichButton(x) {
var initBG = '#fff';
if (x==1)
return "red";
if (x==2)
return "green";
if (x==3)
return "blue";
if (x==4)
return "orange";
if (x==0)
return initBG;
}
// get the buttons into an array
var buttons = [].slice.call(document.getElementsByClassName('button'));
// add event listener to the #navbar element
document.getElementById('navbar').addEventListener('mouseover', function(e){
// target is an element being hovered
var target = e.target;
// check if the target is in the array of buttons
var index = buttons.indexOf( e.target );
if( index > -1 ){
document.body.style.backgroundColor = whichButton(index + 1)
}
else {
document.body.style.backgroundColor = whichButton(0);
}
});
Please scroll down to bold text if you want to go straight to the question
I have made a page that consists of a grid of 9 tiles (divs).
Between 1-9 of those tiles could potentially have a slider inside it.
The sliders are all setup via a jQuery each function e.g
_gridSlider.each(function(){
// count slides, setup slider etc
}); // end slider each function
Everything works fine except the sliders all change at the same time and so I want to add some diversity into the start times.
Right now I create a random ID between 1 and X (X being the number of sliders) inside of the each function for each slider like so
_gridSlider.each(function(){
var _sliderID = Math.floor((Math.random() * _numSliders) + 1);
}); // end slider each function
I then start the sliders at a different time based around this ID like so
var _sliderStart = _sliderID + '000';
setTimeout(function() {
startTimer();
}, _sliderStart);
This works fine the only problem is that it is possible for 2 or more sliders to have the same ID, what I need is to assign each slider an ID between 1 and X but make sure that each slider has a different ID.
The end result would be have 1 timer starting at 1 second, another at 2 seconds, another at 3 seconds etc
You can use this function:
function generateId(numSliders) {
var store = generateId._store;
if (!store) {
generateId._store = {};
}
do {
var id = Math.floor(Math.random()*numSliders*1000+1);
} while (store[id])
store[id] = true;
return id;
}
Then you can use generated id as a start time:
var sliderId = generateId(slidersNumber);
var sliderStart = sliderId; // without + '000'
UPD generateId._store keeps used IDs inside itself. In that function store is used as a property of its function, to not add redundant variable to the namespace. You can put it outside of the generateId function. For example:
var store = {};
function generateId(numSliders) {
do {
var id = Math.floor(Math.random()*numSliders*1000+1);
} while (store[id])
return id;
}
But in that case you're polluting the namespace with redundant variable.
store inside of the function is used just to shorten the code a little. If you want you can write:
function generateId(numSliders) {
if (!generateId._store) {
generateId._store = {};
}
do {
var id = Math.floor(Math.random()*numSliders*1000+1);
} while (generateId._store[id])
generateId._store[id] = true;
return id;
}
An example using shuffle.
function createNumberSeries(howMany) {
var result = [];
for (var count = 1; count <= howMany; count += 1) {
result.push(count);
}
return result;
}
function shuffle(obj) {
var i = obj.length;
var rnd, tmp;
while (i) {
rnd = Math.floor(Math.random() * i);
i -= 1;
tmp = obj[i];
obj[i] = obj[rnd];
obj[rnd] = tmp;
}
return obj;
}
function createDivs(ids) {
var length = ids.length;
for (var index = 0; index < length; index += 1) {
var div = document.createElement('div');
div.id = ids[index];
div.className = 'initial';
document.body.appendChild(div);
}
}
function colourDivs(howMany) {
for (var index = 1; index <= howMany; index += 1) {
setTimeout((function(id) {
return function() {
document.getElementById(id).className += ' colorMe';
};
}(index)), index * 1000);
}
}
var numSliders = 10;
var sliderIds = shuffle(createNumberSeries(numSliders));
createDivs(sliderIds);
colourDivs(numSliders);
.initial {
height: 10px;
width: 10px;
border-style: solid;
border-width: 1px;
}
.colorMe {
background-color: blue
}
I'm trying to assign onmouseover - onmouseout events to an array of divs with a loop.
I created the divs through a loop as well using a function parameter createDivs(x), x being number of divs and a bunch of this.property to assign styles.
Everything is working as expected, but assigning the mouse events through a loop with the divArray.Length object.
The script is the following:
Making the divs:
containers : {
create : function(containerCount){
var cArray = [this.c1Color,this.c2Color,this.c3Color];
var aCounter = 0;
divArray = [];
for (var i = 1; i <= containerCount; i++){
var c = document.createElement("div");
c.id = ("container"+i);
c.style.width = "100%";
c.style.height = (this.height) + "px";
c.style.backgroundColor = (cArray[aCounter]);
aCounter++;
document.body.appendChild(c);
divArray.push(c);
}
}
},
Assigning the Events:
events : {
on : function () {
var z = 1;
for (var i = 0; i < divArray.length; i++){
var cont = ("container" + z);
document.getElementById(divArray[i].id).onmouseover = function(){
gala.animate.openAnimation(cont);
}
document.getElementById(divArray[i].id).onmouseout = function(){
gala.animate.shrinkAnimation(cont);
}
console.log(cont);
z++;
}
}
The console show the array sort through the number of divs as expected, and the cont variable ++ increase to assign the id. However at the end, the event listeners are only applied to the last element of the array.
Btw the cont variable is just a placeholder for a parameter that passes too the animation method so it knows what div to animate, meaning animat.openAnimation(cont) cont = div name.
Looks like you need a new scope to keep the value of the cont variable constant inside the event handlers. I replaced the cont variable as it didn't really seem neccessary
events : {
on : function () {
for (var j = 0; j < divArray.length; j++){
(function(i) {
divArray[i].onmouseover = function(){
gala.animate.openAnimation("container" + (i+1));
}
divArray[i].onmouseout = function(){
gala.animate.shrinkAnimation("container" + (i+1));
}
})(j);
}
}
I am trying to use Javascript to disable a button after it is clicked x amount of times. For simplicity sake lets say x = 2 for now. I cannot seem to get the counter to increment. Thank You for any help!
var $ = function (id) {
return document.getElementById(id);
}
window.onload = function () {
coke.onclick = function(){
var count =0;
if (count >= 1)
{
coke.disabled = true;
}
else
count++;
};
}
Where "coke" is the element ID. If i get rid of the if statement and just have coke.disabled = true, of course it works and disables after one click. I'm sure there is a core concept I am missing.
Thank You
This is happening because each time the onclick event is fired, your var count is being assigned to 0, so it will never be greater than or equal to one in your function. If you initialize the count var outside of the onclick function, it will behave as expected.
window.onload = function () {
var count = 0;
coke.onclick = function(){
if (count >= 1)
{
coke.disabled = true;
}
else
count++;
};
}
You need to define count outside the scope of your onclick function:
var $ = function (id) {
return document.getElementById(id);
}
var count = 0; // set initial count to 0
window.onload = function () {
coke.onclick = function(){
if (count >= 1)
{
coke.disabled = true;
}
else
count++;
};
}