Here's what I'm trying to achieve:
I have a div that switches between fixed and absolute positioning depending on the scroll position. I have an example that works but I noticed that it's a little slow because it constantly changes the position on each and every pixel of scroll. So I thought that adding an additional if statement (as kind of a on and off switch) would remedy it a bit. But of course it broke.
I rarely use jquery/javascript so to my eyes this seems right but it's not.. any help? Maybe there's even a better way of doing this than if statements.
var top = blah;
var bottom = blah;
var ison=0;
$(window).scroll(function (event) {
var y = $(this).scrollTop();
if (y >= top && y <= bottom) {
if (ison===0) {
$('#float-contain').addClass('fixed');
var ison = 1;
}
} else {
if (ison===1) {
$('#float-contain').removeClass('fixed');
var ison = 0;
}
}
});
Try removing the "var" inside each if statement like this:
if (y >= top && y <= bottom) {
if (ison===0) {
$('#float-contain').addClass('fixed');
ison = 1;
}
} else {
if (ison===1) {
$('#float-contain').removeClass('fixed');
ison = 0;
}
}
You're already declaring var ison globally above all of this. By redeclaring the variable inside a function you're creating new local instances of it, which causes some undesirable results. I'm not sure if this is you're only problem, but it's definitely part of it.
By the way, here's a good overview of global and local variable in Javascript. They even include an example of identically named global and local variables (which I try to avoid).
I think the slowness is due to repeated document.getElementById calls due to $('#float-contain'). You can cache the jQuery object so that you don't do it. addClass/removeClass/toggleClass doesn't do anything if the element already has/doesn't have the class.
This should work just fine:
var top = blah;
var bottom = blah;
var $elem = $('#float-contain');
$(window).scroll(function (event) {
var y = $(this).scrollTop();
$elem.toggleClass('fixed', (y >= top && y <= bottom));
});
You can also make this code run when the user has finished scrolling instead of running continuously or you can 'throttle' it to run only once per say 100ms. Here's a way to achieve the first technique - http://www.matts411.com/post/delaying_javascript_event_execution/
Most likely the problem is that the value of ison is always undefined when you try accessing it, because you are defining it inside the function, but only after trying to read it.
Whenever you define a variable inside a function, even if it is not declared until later in the code, it automatically overrides any variables with the same name outside the scope - thus inside the function, the value of ison is undefined until it's defined, and the code never reaches that because of your if clauses.
Try removing var from the var ison = n statements inside the function and that may help. This would make it access the variable you declare in global scope.
Here,
if (ison===0) {
$('#float-contain').addClass('fixed');
var ison = 1;
}
Delete the var because you're redeclaring ison with it. (do the same for the other branch).
Related
I have a page that has a number of divs on it and I am writing a function that loops over all of them, assigns them a random ID and attaches some event listeners to each. However, I am getting the problem outlined above when I check my code using JSHint. I have looked at other threads for this issue, but I do not fully understand how to apply the answers from there in my code. Below is a section that is relevant to this warning:
//global variables
var buttons = document.getElementsByClassName("btn");
var firstClick = 1; //set firstClick flag to 1 on load
var buttonIDs = [];
var clickedButtonID = null;
var clickedButtonColour = null;
var clickedButtonTitle = null;
function addListenersToAllBtns() {
//loop over all buttons on page, assign them unique IDs and attach their EventListeners
for (let i = 0; i < buttons.length; i++) {
let btn_id = guidGenerator();
buttonIDs[i] = btn_id;
buttons[i].setAttribute("id", btn_id);
//add to child node to only have the left side of the button clickable
buttons[i].childNodes[1].addEventListener("click", function () {
//save button information
clickedButtonID = btn_id;
clickedButtonTitle = buttons[i].childNodes[1].childNodes[1].innerHTML;
//button class format is ".btn colour"
clickedButtonColour = buttons[i].getAttribute("class").split(" ")[1];
console.log("The ID of the clicked button is: ", clickedButtonID, "\n",
'The colour of the clicked button is ', clickedButtonColour);
if (firstClick == 1) {
firstClick = 0;
window.addEventListener("resize", positionHabitCard);
document.addEventListener('DOMContentLoaded', positionHabitCard, false);
}
});
buttons[i].childNodes[1].addEventListener("click", showHabitCard);
}
}
I have tried taking the function definition outside the for loop like below and passing it to addEventListener but then it cannot access buttons[i] and has issues with btn_id (says it is undefined).
function getButtonData() {
//save button information
clickedButtonID = btn_id;
clickedButtonTitle = buttons[i].childNodes[1].childNodes[1].innerHTML;
//button class format is ".btn colour"
clickedButtonColour = buttons[i].getAttribute("class").split(" ")[1];
console.log("The ID of the clicked button is: ", clickedButtonID, "\n",
'The colour of the clicked button is ', clickedButtonColour);
if (firstClick == 1) {
firstClick = 0;
window.addEventListener("resize", positionHabitCard);
document.addEventListener('DOMContentLoaded', positionHabitCard, false);
}
}
Could someone more experienced take a look at this and help me out? I am very much a newbie when it comes to JS, so please excuse any glaring errors I may have missed.
Thank you very much in advance.
It's a warning, not an error. I can't really say it's an accurate warning though.
First of all, it's only really "confusing" if you are still using var declarations inside a loop. var declarations are only local to functions and the global scope. So there's no such thing as a var declaration for any loop. You can place a var declaration inside a loop but that won't create multiple variables of it. Instead any reference down the line (say like in a function body) will reference the one and only variable created by the declaration inside the loop.
That can be confusing because any one reference can update the variable. So, the first function created by the first iteration could alter a variable that is being used by every other function inside the loop.
With ES6 you have let and const which introduce block scope which also works within loops. That means a declaration inside a loop that iterates n times can create n variables and each reference inside the block will only apply to that iteration.
You are using let declarations inside the loop so you don't really have a problem there. However, if you were to change let i for var i or let btn_id for var btn_id you'd would see that all your functions created in the loop apply the logic for the last button. That is because every iteration would alter i and btn_id (as there would only be one variable per each across all functions).
I am facing a really odd problem when executing my JS code. My code execution is not entering a for loop and there are no errors in the console. When I try executing the loop by typing directly in the console, it is executing. Here is the loop :
for(var x = 0; x <= distance; x++) {
var yMinusY1Sq = distance*distance - (x - startX)*(x - startX)
var yMinusy1Cal = parseInt(Math.sqrt(yMinusY1Sq));
console.log(yMinusY1Sq, yMinusy1Cal);
if((yMinusY1Sq == yMinusy1Cal*yMinusy1Cal)) {
y = yMinusY1Cal + startY;
var point = document.createElement("div");
point.className = "output-point";
point.style.height = (grid.offsetHeight/16).toString() + "px";
point.style.width = (grid.offsetWidth/16).toString() + "px";
outputPoints.appendChild(point);
point.style.top = y*oneBoxY;
point.style.left = (x+startX)*oneBoxX;
isodistancePoints.push(point);
}
}
Here, distance is >= 1. The console.log inside the loop is not executing and printing anything. While, if another console.log is put just before the loop, it is printing. So, what is going wrong?
Edit
I tried printing distance, which is a global variable, just before the loop, it is showing undefined, but if it is printed directly using the console, it gives a number value. Here is a screenshot:
Here, is the order of initialization and function, the other global variables initialized are working fine.
var distance;
init();
function init() {
distance = 1;
//Other code
}
$("#run").bind('click', function () {
// some other code
for(var x = 0; x <= distance; x++) {
// some code
}
});
The problem you are facing is in the console. What gets logged is a reference not the value of the object in time. From MDN
Please be warned that if you log objects in
the latest versions of Chrome and Firefox what you get logged on the
console is a reference to the object, which is not necessarily the
'value' of the object at the moment in time you call console.log(),
but it is the value of the object at the moment you open the console.
And the solution is
console.log(JSON.stringify(distance))
or
console.log(`${distance}`)
The problem was that the loop was inside an else statement, and the if of the statement had a local variable named distance. I still don't know the exact cause of the error, as the execution is not supposed to go into the if because its condition is not satisfied, but renaming the variable worked and now I am getting desired output. But if anyone knows the exact reason, please comment.
This is so simple I forgot how to do it. I've always passed variables to a function hence it's param's were pre-set, now I need to set the param's when declaring the function, but don't remember the setup.
I'm looking for the working version of this:
function(a,b=4){return a-b;}
Where the b param' of the function is set when the function is declared.
If I remember rightly it's like setting a default for b if the function has no second argument:
function(a,b){b=b || 4; return a-b;}
EDIT
Thanks for all your help but it seems it's impossible in js without ECMAScript 6. Your answers are getting a bit off topic though... I really needed the values set in the paren's.
To keep it on topic... my initial problem is sending parameters to a setTimeout function. Ok so I have a <div> with a .gif background, when clicked it's background changes, this second animation runs for exactly 8 seconds and then the background changes again to a final .gif. so it's a 3 stage animation, simple... thing is the 8sec gap, I figured a setTimeout would work but I can't pass any param's to the 'sto' function to reference said <div>.
If you know of any timer events that can help then be my guest, this is as far as I've got. My original code is below... it fails at function(p = cha).
for(var i = 0; i < 5; i++){
var cha = document.createElement('div');
$(cha).css('background','url(img/stand.gif)');
cha.addEventListener('click',function(){
$(cha).css('background','url(img/walk.gif)');
setTimeout(function(p = cha){
$(p).css('background','url(img/walk.gif)');
},8000);
});
}
function(a,b){b=b || 4; return a-b;}
This is the typical way to default params in ES5. However I would advise changing this to check b's typs a little more strictly, because 0 will be considered a falsey value by the || operator
function(a,b){
b = typeof b === 'undefined' ? 4 : b;
return a-b;
}
I have a simple goal, I would like to increment a variable but I'm facing the closure problem. I've read why this s happening here How do JavaScript closures work?
But I can't find the solution to my problem :/
let's assume this part of code I took from the link.
function say667() {
// Local variable that ends up within closure
var num = 666;
var sayAlert = function() { alert(num); //incrementation
}
num++;
return sayAlert;
}
I would like to increment num within the function and to keep the changes to num.
How could I do that ?
Here is the JsFiddle where I have my problem, I can't figure out how to increment my totalSize and keep it.
http://jsfiddle.net/knLbv/2/
I don't want a local variable that ends up with closure.
From your fiddle, I guess the problem is a mix of closure (totalSize should be outside of the loop) and query.exec being asynchronous (this one can be verified with some console.log).
What you seem to need is some kind of control flow, something like async.reduce
function say667() {
// Local variable that ends up within closure
var num = 666;
var sayAlert = function() { alert(num++); //incrementation
}
return sayAlert;
}
var inscrese = say667();
so if you want to increase by one just call increase();
If all other solutions fail, then make num an array: var num = [666], then increment it's first element: num[0]++.
I've had a hard time debugging a news ticker - which I wrote from scratch using JavaScript.
It works fine on most browsers apart from IE9 (and some mobile browsers - Opera Mobile) where it is moving very slowly.
Using Developer Tools > Profiler enabled me to find the root cause of the problem.
It's a call to offsetLeft to determine whether to rotate the ticker i.e. 1st element becomes the last element.
function NeedsRotating() {
var ul = GetList();
if (!ul) {
return false;
}
var li = GetListItem(ul, 1);
if (!li) {
return false;
}
if (li.offsetLeft > ul.offsetLeft) {
return false;
}
return true;
}
function MoveLeft(px) {
var ul = GetList();
if (!ul) {
return false;
}
var li = GetListItem(ul, 0);
if (!li) {
return false;
}
var m = li.style.marginLeft;
var n = 0;
if (m.length != 0) {
n = parseInt(m);
}
n -= px;
li.style.marginLeft = n + "px";
li.style.zoom = "1";
return true;
}
It seems to be taking over 300ms to return the value, whereas the ticker is suppose to be moving left 1 pixel every 10ms.
Is there a known fix for this?
Thanks
DOM operations
I agree with #samccone that if GetList() and GetListItem() are performing DOM operations each time, you should try to save references to the elements retrieved by those calls as much as possible and reduce the DOM operations.
then I can just manipulate that variable and hopefully it won't go out of sync with the "real" value by calling offsetLeft.
You'll just be storing a reference to the DOM element in a variable. Since it's a reference, it is the real value. It is the same exact object. E.g.:
var li = ul.getElementsByTagName( "li" )[ index ];
That stores a reference to the DOM object. You can read offsetLeft from that object anytime, without performing another DOM operation (like getElementsByTagName) to retrieve the object.
On the other hand, the following would just store the value and would not stay in sync:
var offsetLeft = ul.getElementsByTagName( "li" )[ index ].offsetLeft;
offsetLeft
If offsetLeft really is a bottleneck, is it possible you could rework this to just read it a lot less? In this case, each time you rotate out the first item could you read offsetLeft once for the new first item, then just decrement that value in each call to MoveLeft() until it reaches 0 (or whatever)? E.g.
function MoveLeft( px ) {
current_offset -= px;
If you want to get even more aggressive about avoiding offsetLeft, maybe you could do something where you read the width of each list item once, and the offsetLeft of the first item once, then just use those values to determine when to rotate, without ever calling offsetLeft again.
Global Variables
I think I get it... so elms["foo"] would have to be a global variable?
I think really I just need to use global variables instead of calling offsetLeft every 10 ms.
You don't need to use global variables, and in fact you should avoid it -- it's bad design. There are at least a couple of good approaches you could take without using global variables:
You can wrap your whole program in a closure:
( function () {
var elements = {};
function NeedsRotating() {
...
}
function example() {
// The follow var declaration will block access
// to the outer `elements`
var elements;
}
// Rest of your code here
} )();
There elements is scoped to the anonymous function that contains it. It's not a global variable and won't be visible outside the anonymous function. It will be visible to any code, including functions (such as NeedsRotating() in this case), within the anonymous function, as long as you don't declare a variable of the same name in your inner functions.
You can encapsulate everything in an object:
( function () {
var ticker = {};
ticker.elements = {};
// Assign a method to a property of `ticker`
ticker.NeedsRotating = function () {
// All methods called on `ticker` can access its
// props (e.g. `elements`) via `this`
var ul = this.elements.list;
var li = this.elements.list_item;
// Example of calling another method on `ticker`
this.do_something();
} ;
// Rest of your code here
// Something like this maybe
ticker.start();
} )();
Here I've wrapped everything in an anonymous function again so that even ticker is not a global variable.
Response to Comments
First of all, regarding setTimeout, you're better off doing this:
t = setTimeout( TickerLoop, i );
rather than:
t = setTimeout("TickerLoop();", i);
In JS, functions are first-class objects, so you can pass the actual function object as an argument to setTimeout, instead of passing a string, which is like using eval.
You may want to consider setInterval instead of setTimeout.
Because surely any code executed in setTimeout would be out of scope of the closure?
That's actually not the case. The closure is formed when the function is defined. So calling the function via setTimeout does not interfere with the function's access to the closed variables. Here is a simple demo snippet:
( function () {
var offset = 100;
var whatever = function () {
console.log( offset );
};
setTimeout( whatever, 10 );
} )();
setTimeout will, however, interfere with the binding of this in your methods, which will be an issue if you encapsulate everything in an object. The following will not work:
( function () {
var ticker = {};
ticker.offset = 100;
ticker.whatever = function () {
console.log( this.offset );
};
setTimeout( ticker.whatever, 10 );
} )();
Inside ticker.whatever, this would not refer to ticker. However, here you can use an anonymous function to form a closure to solve the problem:
setTimeout( function () { ticker.whatever(); }, 10 );
Should I store it in a class variable i.e. var ticker.SecondLiOffsetLeft = GetListItem(ul, 1).offsetLeft then I would only have to call offsetLeft again when I rotate the list.
I think that's the best alternative to a global variable?
The key things are:
Access offsetLeft once each time you rotate the list.
If you store the list items in a variable, you can access their offsetLeft property without having to repeatedly perform DOM operations like getElementsByTagName() to get the list objects.
The variable in #2 can either be an object property, if you wrap everything up in an object, or just a variable that is accessible to your functions via their closure scope. I'd probably wrap this up in an object.
I updated the "DOM operations" section to clarify that if you store the reference to the DOM object, it will be the exact same object. You don't want to store offsetLeft directly, as that would just be storing the value and it wouldn't stay in sync.
However you decide to store them (e.g. object property or variable), you should probably retrieve all of the li objects once and store them in an array-like structure. E.g.
this.li = ul.getElementsByTagName( "li" );
Each time you rotate, indicate the current item somehow, e.g.:
this.current_item = ###;
// or
this.li.current = this.li[ ### ];
// Then
this.li[ this.current_item ].offsetLeft
// or
this.li.current.offsetLeft
Or if you want you could store the li objects in an array and do this for each rotation:
this.li.push( this.li.shift() );
// then
this.li[0].offsetLeft
if you dont cache your selectors in var li = GetListItem(ul, 1);
then performance will suffer greatly.. and that is what you are seeing because you are firing up a new selector every 10ms
you should cache the selector in a hash like
elms["foo"] = elms["foo"] || selectElm(foo);
elms["foo"].actionHere(...)
your code is slow because reading offsetLeft will force the browser to do a reflow. the reflow is the part that is slowing you down. the browser is typically smart enough to queue changes to reduce the number of reflows. however, given that you want the most up to date value when access offsetLeft, you're forcing the browser to flush that queue and do a reflow in order to calculate the correct value for you.
without knowing all the details of what you're trying to do, it's hard to know what to recommend to improve performance. http://www.phpied.com/rendering-repaint-reflowrelayout-restyle/ explains this problem in more detail and offers some advice about minimizing reflows.