I'm appending onclick events to elements that I'm creating dynamically. I'm using the code below, this is the important part only.
Test.prototype.Show= function (contents) {
for (i = 0; i <= contents.length - 1; i++) {
var menulink = document.createElement('a');
menulink.href = "javascript:;";
menulink.onclick = function () { return that.ClickContent.apply(that, [contents[i]]); };
}
}
First it says that it's undefined. Then I changed and added:
var content = content[i];
menulink.onclick = function () { return that.ClickContent.apply(that, [content]); };
What is happening now is that it always append the last element to all onclick events( aka elements). What I'm doing wrong here?
It's a classical problem. When the callback is called, the loop is finished so the value of i is content.length.
Use this for example :
Test.prototype.Show= function (contents) {
for (var i = 0; i < contents.length; i++) { // no need to have <= and -1
(function(i){ // creates a new variable i
var menulink = document.createElement('a');
menulink.href = "javascript:;";
menulink.onclick = function () { return that.ClickContent.apply(that, [contents[i]]); };
})(i);
}
}
This immediately called function creates a scope for a new variable i, whose value is thus protected.
Better still, separate the code making the handler into a function, both for clarity and to avoid creating and throwing away builder functions unnecessarily:
Test.prototype.Show = function (contents) {
for (var i = 0; i <= contents.length - 1; i++) {
var menulink = document.createElement('a');
menulink.href = "javascript:;";
menulink.onclick = makeHandler(i);
}
function makeHandler(index) {
return function () {
return that.ClickContent.apply(that, [contents[index]]);
};
}
};
A way to avoid this problem altogether, if you don't need compatibility with IE8, is to introduce a scope with forEach, instead of using a for loop:
Test.prototype.Show = function (contents) {
contents.forEach(function(content) {
var menulink = document.createElement('a');
menulink.href = "javascript:;";
menulink.onclick = function() {
return that.ClickContent.call(that, content);
};
});
}
Related
My code does work but I don't want the jshint errors anymore:
Functions declared within loop referencing an outer scoped variable may lead to confusing semantics
I've tried using let from ES6 to get around the error because I thought that would solve the problem. I configured my gruntfile to use ES6 as well.
I tried using two loops, the outer loop with variable 'i' and the inner loop with variable 'j'
Neither worked.
Full code provided here: https://jsfiddle.net/rwschmitz/zz7ot3uu/
var hobbies = document.getElementsByClassName("hobbies");
var active = false;
// For mouse input
for (var i = 0; i < 5; i++) {
hobbies[i].onmouseover = function() {
hobbies[0].classList.add('hobbies-slide-left');
hobbies[1].classList.add('hobbies-slide-right');
hobbies[2].classList.add('hobbies-slide-left');
hobbies[3].classList.add('hobbies-slide-right');
hobbies[4].classList.add('hobbies-slide-left');
};
}
// For click input
for (var i = 0; i < 5; i++) {
hobbies[i].onclick = function() {
hobbies[0].classList.add('hobbies-slide-left');
hobbies[1].classList.add('hobbies-slide-right');
hobbies[2].classList.add('hobbies-slide-left');
hobbies[3].classList.add('hobbies-slide-right');
hobbies[4].classList.add('hobbies-slide-left');
};
}
You could change your loops to something like this, using Array#forEach():
var hobbies = Array.from(document.getElementsByClassName('hobbies'));
var classes = ['hobbies-slide-left', 'hobbies-slide-right'];
var events = ['mouseover', 'click'];
function addHobbyClass (hobby, index) {
hobby.classList.add(this[index % this.length]);
}
function hobbyEventListener () {
hobbies.forEach(addHobbyClass, classes);
}
hobbies.forEach(function (hobby) {
this.forEach(function (event) {
this.addEventListener(event, hobbyEventListener);
}, hobby);
}, events);
Two additional examples of how to fix the problem.
var hobbies = document.querySelectorAll('.hobbies');
var eventHooks = ['mouseover', 'click'];
hobbies.forEach(function(hobby) {
eventHooks.forEach(function(hook) {
hobby.addEventListener(hook, function() {
hobbies[0].classList.add('hobbies-slide-left');
hobbies[1].classList.add('hobbies-slide-right');
hobbies[2].classList.add('hobbies-slide-left');
hobbies[3].classList.add('hobbies-slide-right');
hobbies[4].classList.add('hobbies-slide-left');
});
});
});
var hobbies = document.getElementsByClassName('hobbies');
var eventHooks = ['mouseover', 'click'];
// Attach events
var attachEvents = function(key) {
eventHooks.forEach(function(hook) {
hobbies[key].addEventListener(hook, function() {
hobbies[0].classList.add('hobbies-slide-left');
hobbies[1].classList.add('hobbies-slide-right');
hobbies[2].classList.add('hobbies-slide-left');
hobbies[3].classList.add('hobbies-slide-right');
hobbies[4].classList.add('hobbies-slide-left');
});
});
};
// Init
var init = function() {
// Loop through hobbies
for (var i = 0; i < hobbies.length; i++) {
attachEvents(i);
}
}
init();
I have a SCOPE problem. When I declare "var text" outside the function all works. But inside the function it works only in the first part. Here is what I mean:
This is a buffer function. Executing buffer("anything") saves "anything". Executing buffer() - without the properties will return all properties.
buffer("Al")
buffer("ex")
buffer() <= should return Alex
But the SCOPE of "text" is wrong and it does not return the saved properties.
function makeBuffer() {
var text = "";
if (arguments.length != 0) {
for (let i = 0; i < arguments.length; i++) {
console.log(`Adding argument - (${arguments[i]})`);
text += arguments[i];
console.log(`New text - (${text})`);
}
} else {
console.log(`text - (${text})`);
return text;
}
}
var buffer = makeBuffer;
buffer("One", "Two");
document.write(buffer());
That is normal behaviour.
A variable defined in a given scope goes away when the scope goes away. Each call the to the function creates a new scope.
Declaring the variable outside the function is the standard way to share a value between invocations of it.
What you want is a factory:
function makeBuffer() {
var text = "";
return function buffer() {
if (arguments.length != 0) {
for (let i = 0; i < arguments.length; i++) {
console.log(`Adding argument - (${arguments[i]})`);
text += arguments[i];
console.log(`New text - (${text})`);
}
} else {
console.log(`text - (${text})`);
return text;
}
}
}
var buffer = makeBuffer();
buffer("One", "Two");
document.write(buffer());
You could do this using an object. This will make your code much more organized.
var Buffer = function() {
this.text = "";
}
Buffer.prototype.append = function() {
for (var i = 0; i < arguments.length; i++) {
this.text += arguments[i];
}
}
Buffer.prototype.get = function() {
return this.text;
}
var buffer = new Buffer();
buffer.append("One", "Two");
document.write(buffer.get());
Using ES6 the syntax gets even sweeter:
class Buffer {
constructor() {
this.text = "";
}
append() {
this.text = this.text.concat(...arguments);
}
get() {
return this.text;
}
}
var buffer = new Buffer();
buffer.append("One", "Two");
document.write(buffer.get());
As Quentin properly pointed out in his answer, that is a normal behavior.
An alternative option to keep the value on your function without declaring the variable outside is scope is to add it as a property to the function itself.
As in JavaScript a function is a first class object, you can put such data directly into function object (like in any other objects).
An example below, please note how to get property text from your function (buffer.text).
function makeBuffer() {
makeBuffer.text = "";
if (arguments.length != 0) {
for (let i = 0; i < arguments.length; i++) {
console.log(`Adding argument - (${arguments[i]})`);
makeBuffer.text += arguments[i];
console.log(`New text - (${makeBuffer.text})`);
}
} else {
console.log(`text - (${makeBuffer.text})`);
return makeBuffer.text;
}
}
var buffer = makeBuffer;
buffer("Al", "ex");
console.log(`buffer.text - (${buffer.text})`);
Alternatively consider using a closure in order to keep the value of text between function calls.
Closures are functions that refer to independent (free) variables
(variables that are used locally, but defined in an enclosing scope).
In other words, these functions 'remember' the environment in which
they were created. More info here.
let makeBuffer = function() {
// closure
let text = "";
return function() {
if (arguments.length != 0) {
for (let i = 0; i < arguments.length; i++) {
console.log(`Adding argument - (${arguments[i]})`);
text += arguments[i];
console.log(`New text - (${text})`);
}
} else {
console.log(`text - (${text})`);
return text;
}
}
};
var buffer = makeBuffer();
buffer("Al", "ex");
I have 3 methods
exports.getImageById = function (resultFn, id) {
...
}
exports.getCollectionById = function (resultFn, id) {
}
in the third method I want to call both methods
exports.getCollectionImages = function (resultFn, collectionId) {
var arr = new Array();
this.getCollectionById( // fine, 1st call
function (result) {
var images = result.image;
for (i = 0; i < images.length; i++) {
this.getImageById(function (result1) { // error, 2nd call
arr[i] = result1;
}, images[i]
);
}
}
, collectionId
);
resultFn(arr);
}
I can call first function this.getCollectionById but it fails to call this.getImageById, it says undefined function, whats the reason for that?
When you call this.getCollectionById passing it a callback, the callback doesn't have access to the same this
The simplest solution is to save this as a local variable.
exports.getCollectionImages = function (resultFn, collectionId) {
var arr = new Array();
var me = this; // Save this
this.getCollectionById( // fine, 1st call
function (result) {
var images = result.image;
for (var i = 0; i < images.length; i++) {
// Use me instead of this
me.getImageById(function (result1) { // error, 2nd call
arr[i] = result1;
}, images[i]);
}
}, collectionId);
resultFn(arr);
}
The value of this inside the inner function is not the same object as outside, because it's determined depending on how the function is called. You can find a detailed explanation in the MDN article on this.
One of the ways to solve it is by keeping a reference to the outer this in another variable such as that:
var that = this;
this.getCollectionById( // fine, 1st call
function (result) {
var images = result.image;
for (i = 0; i < images.length; i++) {
that.getImageById(function (result1) { // 2nd call
arr[i] = result1;
}, images[i]
);
}
}
, collectionId
);
I am using anonymous function assigned to a variable to minimize use of global variables. Within this function there are nested functions: one to preload and resize images, and two other nested functions for navigation (next and previous). The code below generates error that the variable to which the anonymous function is assigned is not defined:
Cannot read property 'preload_and_resize' of undefined
If you spot the problem please let me know. Thank you very much.
<html>
<head>
<script type="text/javascript">
var runThisCode=(function(){
var myImages=new Array("img/01.jpg","img/02.jpg","img/03.jpg");
var imageObj = new Array();
var index=0;
var preload_and_resize=function(){
var i = 0;
var imageArray = new Array();
for(i=0; i<myImages.length; i++) {
imageObj[i] = new Image();
imageObj[i].src=myImages[i];
}
document.pic.style.height=(document.body.clientHeight)*0.95;
};
var next_image=function(){
index++;
if(index<imageObj.length){
document.pic.src=imageObj[index].src;
}
else{
index=0;
document.pic.src=imageObj[index].src;
}
};
var prev_image=function(){
index--;
if(index>=0){
document.pic.src=imageObj[index].src;
}
else{
index=myImages.length-1;
document.pic.src=imageObj[index].src;
}
};
})();
</script>
</head>
<body onload="runThisCode.preload_and_resize();">
<div align="center">
<img name="pic" id="pic" src="img/01.jpg"><br />
PrevNext
</div>
</body>
</html>
Your anonymous function doesn't return anything, so when you run it, undefined gets returned. That's why runThisCode is undefined. Regardless though, with the way you've written it, preload_and_resize will be local, so you wouldn't be able to access that anyway.
Instead, you want this anonymous function to construct an object, and reutrn that. Something like this should work, or at least get you close:
var runThisCode=(function(){
var result = {};
result.myImages=new Array("img/01.jpg","img/02.jpg","img/03.jpg");
result.imageObj = new Array();
result.index=0;
result.preload_and_resize=function(){
var i = 0;
var imageArray = new Array();
for(i=0; i< result.myImages.length; i++) {
imageObj[i] = new Image();
imageObj[i].src=myImages[i];
}
document.pic.style.height=(document.body.clientHeight)*0.95;
};
result.next_image=function(){
index++;
if(index<imageObj.length){
document.pic.src=imageObj[index].src;
}
else{
index=0;
document.pic.src=imageObj[index].src;
}
};
result.prev_image=function(){
index--;
if(index>=0){
document.pic.src=imageObj[index].src;
}
else{
index=myImages.length-1;
document.pic.src=imageObj[index].src;
}
};
return result;
})();
This should explain what you are doing wrong :
var foobar = (function (){
var priv1, priv2 = 'sum' , etc;
return {
pub_function: function() {},
another: function() {
console.log('cogito ergo ' + priv2 );
}
};
})();
foobar.another();
You've assigned the function to the variable next_image which is scoped to the self-invoking anonymous function.
The value you assign to runThisCode is the return value of that anonymous function, which (since there is no return statement) is undefined.
To get the code to work you need to assign an object to runThisCode and make next_image a member of it.
Add the following to the end of the anonymous function:
return {
"next_image": next_image
}
Remove the anonymous function, and make your function public. You will only create one global variable: the object runThisCode.
var runThisCode = function () {
var myImages = new Array("img/01.jpg", "img/02.jpg", "img/03.jpg");
var imageObj = new Array();
var index = 0;
this.preload_and_resize = function () {
var i = 0;
var imageArray = new Array();
for (i = 0; i < myImages.length; i++) {
imageObj[i] = new Image();
imageObj[i].src = myImages[i];
}
document.pic.style.height = (document.body.clientHeight) * 0.95;
};
this.next_image = function () {
index++;
if (index < imageObj.length) {
document.pic.src = imageObj[index].src;
} else {
index = 0;
document.pic.src = imageObj[index].src;
}
};
this.prev_image = function () {
index--;
if (index >= 0) {
document.pic.src = imageObj[index].src;
} else {
index = myImages.length - 1;
document.pic.src = imageObj[index].src;
}
};
};
And then, later in your code:
runThisCode.preload_and_resize();
should work.
From the invocation you've got in body onload property, it looks like what you're trying to achieve with the IIFE (immediately invoked function expression) is return an object that has a the method preload_and_resize.
As others have pointed out, you're not returning anything from the IIFE, so really all that's happening is you're closing up everything inside it in its own namespace, but not "exporting" anything.
If you want to "export" those functions, from your IIFE, you'd probably add a final bit to it that looked something like this:
return {
'preload_and_resize': preload_and_resize,
'next_image': next_image,
'prev_image': prev_image
}
which essentially creates a new JavaScript object literal, and then assigns its properties to the function values from the local scope.
Some developers would find this redundant and rather than finishing out with this sort of explicit export would probably just define the functions while declaring the object literal, something like:
return {
preload_and_resize: function(){
var i = 0;
var imageArray = new Array();
for(i=0; i<myImages.length; i++) {
imageObj[i] = new Image();
imageObj[i].src=myImages[i];
}
document.pic.style.height=(document.body.clientHeight)*0.95;
},
next_image: function() {
index++;
if(index<imageObj.length){
document.pic.src=imageObj[index].src;
}
else {
index=0;
document.pic.src=imageObj[index].src;
}
},
prev_image: function() {
index--;
if(index>=0){
document.pic.src=imageObj[index].src;
}
else {
index=myImages.length-1;
document.pic.src=imageObj[index].src;
}
}
}
In respect of previous answers, my version:
function(self) {
let myImages = new Array("img/01.jpg", "img/02.jpg", "img/03.jpg");
let imageObj = new Array();
let index = 0; // if you need to expose this call with self.index
self.preload_and_resize = function() {
let i = 0;
let imageArray = new Array();
let (i = 0; i < myImages.length; i++) {
imageObj[i] = new Image();
imageObj[i].src = myImages[i];
}
document.pic.style.height = (document.body.clientHeight) * 0.95;
};
var next_image = function() {
index++;
if (index < imageObj.length) {
document.pic.src = imageObj[index].src;
} else {
index = 0;
document.pic.src = imageObj[index].src;
}
};
var prev_image = function() {
index--;
if (index >= 0) {
document.pic.src = imageObj[index].src;
} else {
index = myImages.length - 1;
document.pic.src = imageObj[index].src;
}
};
})(window.myCurrentPage = window.myCurrentPage || {});
// now you canll myCurrentPage.preload_and_resize();
I have this javascript snippet:
var selectName["id1","id2","id3"];
setOnClickSelect = function (prefix, selectName) {
for(var i=0; i<selectName.length; i++) {
var selId = selectName[i];
alert(selId);
$(selId).onchange = function() {
$(selId).value = $(selId).options[$(selId).selectedIndex].text;
}
}
}
But when I change value to my id1 element, the alert wrote me always "id3".
Can I fix it?
EDIT:
I've changed my snippet with these statements:
setOnChangeSelect = function (prefix, selectName) {
for(var i=0; i<selectName.length; i++) {
var selId = selectName[i];
$(selId).onchange = (function (thisId) {
return function() {
$(selId).value = $(thisId).options[$(thisId).selectedIndex].text;
}
})(selId);
}
}
But selId is always the last element.
This is caused by the behavior of javaScript Closure, selId has been set to the selectName[2] at the end of the loop and that's why you get 'id3' back.
An fix is as following, the key is wrap the callback function inside another function to create another closure.
var selectName = ["id1","id2","id3"];
var setOnClickSelect = function (prefix, selectName) {
for(var i = 0; i < selectName.length; i++) {
var selId = selectName[i];
$(selId).onchange = (function (thisId) {
return function() {
$(thisId).value = $(thisId).options[$(thisId).selectedIndex].text;
}
})(selId);
}
};
Ps: there is synyax error for var selectName["id1","id2","id3"], you should use var selectName = ["id1","id2","id3"];