Best practice: functions within loops - javascript

I got the following code which works perfectly. What it does is: in a table it highlights the corresponding table header cell and table first column cell when you hover over any table cell.
// Row & Column Highlight
(function() {
var gridCellRow = null,
gridCellCol = null,
tableElement = document.getElementsByClassName('inner_table');
for (var i = 0, len_i = tableElement.length; i < len_i; i++) {
if (tableElement[i].getElementsByClassName('row_label_cell').length > 0) {
var gridCell = tableElement[i].getElementsByClassName('input_cell');
for (var j = 0, len_j = gridCell.length; j < len_j; j++) {
function gridCellParents(currentCell) {
return gridCellRow = currentCell.parentNode.firstElementChild,
gridCellCol = currentCell.parentNode.parentNode.rows[0].cells[currentCell.cellIndex];
}
gridCell[j].addEventListener('mouseover', (function() {
gridCellParents(this);
gridCellRow.classList.add('highlight');
gridCellCol.classList.add('highlight');
}));
gridCell[j].addEventListener('mouseout', (function() {
gridCellRow.classList.remove('highlight');
gridCellCol.classList.remove('highlight');
}));
}
}
}
}());
However, JSHint tells me, that
for (var j = 0, len_j = gridCell.length; j < len_j; j++) {
function gridCellParents(currentCell) {
return gridCellRow = currentCell.parentNode.firstElementChild,
gridCellCol = currentCell.parentNode.parentNode.rows[0].cells[currentCell.cellIndex];
}
is not best practice "Function declarations should not be placed in blocks. Use a function expression or move the statement to the top of the outer function."
as well as
gridCell[j].addEventListener('mouseover', (function() {
gridCellParents(this);
gridCellRow.classList.add('highlight');
gridCellCol.classList.add('highlight');
}));
gridCell[j].addEventListener('mouseout', (function() {
gridCellRow.classList.remove('highlight');
gridCellCol.classList.remove('highlight');
}));
}
is not best practice "Don't make functions within a loop."
So how am I correctly and according to best practice building this whole function?

Function deceleration shouldn't be within loops because it makes no
sense to re-create the same function over and over again, in a
"continuous flow" (unlike other situation where the same function
might be created again, in a more complex code). The main reason is
because of hoisting and it strongly goes against javascript
principles to write functions declarations inside loops.
A good starting point, with a more ordered code:
// Row & Column Highlight
(function() {
var gridCellRow,
gridCellCol,
gridCell,
tableElement = document.getElementsByClassName('inner_table');
function gridCellParents(currentCell) {
gridCellRow = currentCell.parentNode.firstElementChild,
gridCellCol = currentCell.parentNode.parentNode.rows[0].cells[currentCell.cellIndex];
}
function onMouseEnter() {
gridCellParents(this);
gridCellRow.classList.add('highlight');
gridCellCol.classList.add('highlight');
}
function onMuoseLeave() {
gridCellRow.classList.remove('highlight');
gridCellCol.classList.remove('highlight');
}
for (var i = 0, len_i = tableElement.length; i < len_i; i++) {
if (tableElement[i].getElementsByClassName('row_label_cell').length > 0) {
gridCell = tableElement[i].getElementsByClassName('input_cell');
for (var j = 0, len_j = gridCell.length; j < len_j; j++) {
gridCell[j].addEventListener('mouseenter', onMouseEnter);
gridCell[j].addEventListener('mouseleave', onMuoseLeave);
}
}
}}());
As you can see, I've modified your events to mousenter and mouseleave which might better suit your needs and be better for overall performance.
Update - delegated version:
// Row & Column Highlight
(function() {
var gridCell,
tableElement = document.querySelectorAll('.inner_table');
function getCellParents(cell){
return {
row : cell.parentNode.firstElementChild, // row
col : cell.parentNode.parentNode.rows[0].cells[cell.cellIndex] // col
};
}
function updateGridCellParents(cell, state) {
state = state ? 'add' : 'remove';
var parents = getCellParents(cell);
parents.row.classList[state]('highlight');
parents.col.classList[state]('highlight');
}
funciton checkTarget(target){
// make sure the element is what we expected it to be
return target.className.indexOf('input_cell') != 0;
}
function onMouseEvents(e){
checkTarget(e.target) && updateGridCellParents(e.target, e.type == "mouseover");
}
document.body.addEventListener('mouseover', onMouseEvents);
document.body.addEventListener('mouseout', onMouseEvents);
})();

In addition to the previous answer, I think it is important to also state why it is a bad practice.
The issue when creating functions inside loops is that they often use values that depends on the loop's iteration. Let's have an example.
// Create three function, that writes their number
var funcs = [];
for(var i=0; i<3; i++){
funcs.push(function(){
document.write(i);
});
}
// Call them.
funcs.forEach(function(f){
f();
});
One may expect the above code to write 1 then 2 then 3. However, because variables in JS are not block-scoped but function-scoped (except for the new let and const), the closure of all three of these functions will actually use the exact same i: 3, the last value it had been given (and thus the value it still has).
Because of this behaviour, this is very easy to make mistakes. Hence, it is not recommended.
If you need to create a function that depends of the value of a loop, you can use a factory.
// Create a factory function that returns a
// function that writes the argument.
function writerFactory(msg){
return function(){
document.write(msg);
}
}
// Create three functions, that write their number.
var funcs = [];
for(var i=0; i<3; i++){
funcs.push(writerFactory(i));
}
// Call them.
funcs.forEach(function(f){
f();
});
This time, each function has a different closure: the one that is created by each call of the factory. They all have access to a different msg.

Related

Event listener with anonymous function (typeError , toggle undefined)

I am new to Javascript.
I have two identical tables laid side by side. I would like to have a mirror effect. case 1 works fine with no anonymous function.
However, there seems to be some problem in my case 2 Javascript code which is vital for my project
CASE 1
var table1td = document.querySelectorAll("#table1 td");
var table2td = document.querySelectorAll("#table2 td");
for(var i=0; i<table2td.length; i++)
{
table2td[i].addEventListener("click",_click);
}
function _click() {
this.classList.toggle("_changecolor");
}
CASE 2
var table1td = document.querySelectorAll("#table1 td");
var table2td = document.querySelectorAll("#table2 td");
for(var i=0; i<table2td.length; i++)
{
table2td[i].addEventListener("click", function(){_click(i)});
}
function _click(index) {
this.classList.toggle("_changecolor");
table2td[9-index].classList.toggle("_changecolor");
}
(no changes in HTML,CSS code)
There are two problems with your code:
You can't use this inside _click because it won't be your element. The element that fired the event will be bound to the anonymous function passed to addEventListener, not to _click (which, depeneding on the rest of your code, will either be bound to undefined or the global object window).
You can fix that by explicitly setting the this value when you call _click from within the anonymous function using Function#call or Function#apply:
for(var i = 0; i < table2td.length; i++) {
table2td[i].addEventListener("click", function() {
_click.call(this); // passing the current this (of the anonymous function) to _click as we invoke it
});
}
function _click() {
this.classList.toggle("_changecolor"); // using this is OK
}
You can't use the indexes due to this famous problem.
A quick fix will be to use let (which respects the block scope) instead of var (which doesn't):
for(let i = 0; i < table2td.length; i++) { // using let instead of var
table2td[i].addEventListener("click", function() { _click(i); });
}
function _click(index) { // using index is also OK
table2td[index].classList.toggle("_changecolor");
}

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);
}

angularjs - ng-repeat being called before scope variable is defined

I have a md-tabs setup, I have it binded to $scope.selectedIndex so that I can change it by code when I need to.
I use a button, call my function that updates data, I then change $scope.selectedIndex to the tab number needed (In this case 3) that will then change the selected tab.
All of that is fine, except it's calling $scope.selectedIndex before .forEach() call is finished, which results in a ng-repeat not working as it exits silently with no errors since its not defined.
Button Calls:
ng-click="initTVShow(show)"
Function:
$scope.initTVShow = function(show) {
var rng = "";
var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
for( var i=0; i < 10; i++ )
rng += possible.charAt(Math.floor(Math.random() * possible.length));
$scope.show = show;
$scope.show_name = show.title.split(' ').join('') + "-" + rng;
$scope.poster = show.images.poster;
$scope.backdrop = show.images.poster;
$scope.seasons = [{},{}];
getShow(show.imdb_id, function (err, show) {
for (let i = 0; i < 10; i++) {
$scope.seasons[i] = show.episodes.filter((episode) => episode.season === i);
}
});
$scope.selectedIndex=3;
};
As you can see, $scope.selectedIndex=3; basically changes to Tab #4 (0-based).
Then the following ng-repeat does not seem to work and most likely because .forEach hasn't finished yet. I tested with $scope.seasons with 2 empty index's to test and that works.
<span ng-repeat="season in seasons">{{season.number}}</span>
I think that this is because the function getShow retrieves data asynchronously. Show it to us.
Try this:
getShow(show.imdb_id, function (err, show) {
show.episodes.forEach(function(array){
$scope.seasons[array.season] = array;
});
$scope.selectedIndex=3;
});
It's because the data not ready and the DOM is.
you can solve it easily by adding ng-if on the element and check if the data ready.
Like:<span ng-if="season.number" ng-repeat="season in seasons">{{season.number}}</span>
I found the solution, it was actually just caused by the for() loop (Which pre-edit, was a .forEach), I simple added a callback to it, which changed the tab and it just WORKED!
Changed:
for (let i = 0; i < 11; i++) { }
To:
for (let i = 0; i < 11 || function(){ $scope.selectedIndex=3; }(); i++) { }

Javascript constructor copies array as empty when it should contain items

My code:
function BayesNet(vars) {
this.variables = {};
this.numVars = Object.keys(this.variables).length;
for (v in vars) {
this.variables[v] = new BayesNode(vars[v]);
this.variables[v].CPTorder = this.generateDomainRows(this.variables[v].parents);
this.variables[v].fullCPT = {}
for (var i = 0; i < this.variables[v].CPTorder.length; i++) {
this.variables[v].fullCPT[this.variables[v].CPTorder[i]] = this.variables[v].CPT[i];
}
this.variables[v].blocks = false;
}
}
function BayesNode(obj) {
this.parents = obj.parents;
this.children = obj.children;
if (typeof obj.domain == 'undefined')
this.domain = ['T','F'];
else
this.domain = obj.domain;
this.observation = obj.observation;
this.CPT = obj.CPT;
this.sampleDistribution = [];
for (var i = 0; i < this.CPT.length; i++) {
var s = [];
if(this.CPT[i].length == this.domain.length - 1)
this.CPT[i].push(1 - sumArray(this.CPT[i]));
s.push(this.CPT[i][0]);
for (var j = 1; j < this.domain.length - 1; j++) {
s.push(this.CPT[i][j]+s[j-1]);
}
s.push(1.0);
this.sampleDistribution.push(s);
}
//TODO: Check if CPT is valid
}
My problem is that BayesNode.parent is copied incorrectly.
BayesNode.parent should be an array containing items, and when I run the debugger through the constructor, this.parents is the correct value . However, once I go back to the BayesNet constructor, parents is an empty array. What could be causing this? All other variables in the object behave as expected.
Javascript executes function calls asynchronously. This is the root cause of your issue. You should use callbacks to execute code that is dependent on results of function calls.
Let me explain this using your code:
this.variables[v] = new BayesNode(vars[v]);
this.variables[v].CPTorder = this.generateDomainRows(this.variables[v].parents);
When you call the constructor, JS does NOT wait for the function to finish executing before moving onto the next line of code. When JS comes across "this.variables[v].parents", it is empty, because, the function call in the previous line is still executing asynchronously.
Javascript code design requires a different approach as compared to most other languages.
I don't see any issues in your code, its strange why its becoming empty. but to solve the problem there is a way. change the code as follow.
after this line
this.variables[v] = new BayesNode(vars[v]);
Add the follow
this.variables[v].parents = vars[v].parents;
I see you are not modifying the parents in the constructor, it will work for time being before you find out whats happening. you might have done this already :)

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...
}
}

Categories