How do I keep an array reference through closure? - javascript

Quick question that i seem unable to google...
I'm wondering how to keep an array reference through closure. I think.
Im using AngularJS but raw JS has the same behaviour in my trials.
Context: think of a memory game where you match two images. When two selected tiles mismatch, they flip back but only after showing the selected images. So I want to delay the flip.
I have an array that i .filter to a new array.
$scope.selectedTiles = $scope.tiles.filter($scope.filterSelected);
An aside: This new array; does it only contain references or copies of data? MDN says
constructs a new array of all the values for which callback returns a true value.
Not very clear imo. My conclusion is they are references.
Anyways...
This selectedTiles is sent to a closure in order to keep its references. Only it doesnt.
flipSelectedTiles = function (selection){
return function(){
selection.forEach($scope.flipTile);
}
};
function reset(selection){
return function(){
$scope.flipSelectedTiles(selection);
}
}
var resetTiles = reset(filteredTiles);
$timeout($scope.flipSelectedTiles($scope.selectedTiles), $scope.delay);
I need this because I intend to wait a while before executing the flipSelectedTiles.
I also need to clear the selectedTiles immediately in case someone clicks another image.
selectedTiles.splice(0, $scope.selectedTiles.length);
As the timeout fires the array is empty and nothing happens.
How do I keep an array reference through closure?
Edit: here is a fiddle in plain JS http://jsfiddle.net/Tobis/TkjEF/
Edit: added flipSelectedTiles function

You are keeping a reference. The problem is you're emptying the array too early, as you noted yourself. I believe you need a copy instead:
var resetTiles = reset(filteredTiles.slice(0));
$timeout(resetTiles, $scope.delay);
Also note that you weren't passing a function to $timeout, you were invoking flipSelectedTiles immediately (unless flipSelectedTiles returns a function).

Related

Create a unique function name on the fly for shared timer

I'll start with the exact nature of the problem and then give some background. I am trying to name a function -threadTimer- and give it a random unique identifier, such as 'threadTimer'+ID. A randomly generated ID would work fine. Then, I need to use setInterval on it, to make it fire repeatedly and therein lies my coding problem. I have tried every variation of new, function, function as an object and I just can't get my head around it. You'll notice that the function I have created is an object and perhaps this is where I'm going in circles.
OK, the background I mentioned. threadTimer is fired by a master timer co-ordinating several threads. That's why you'll see I have generated a 'global' object for reference elsewhere. similar HTML entities can fire threadTimer at the same time, hence my requirement to make each instance unique.
window['GlblThreadExe'+ID]=setInterval(function(){threadTimer(elid,parent,lft,top,diameter,point,bStyle,color,grp,startTime,size,ID,counter,div,divwth,divht,wthIncrement,htIncrement,lftStart,topStart,lftIncrement,topIncrement)},interval);
function threadTimer(elid,parent,lft,top,diameter,point,bStyle,color,grp,startTime,size,ID,counter,div,divwth,divht,wthIncrement,htIncrement,lftStart,topStart,lftIncrement,topIncrement){
// more code
}
In truth, I think its the volume of parameters that I'm passing that's confusing my syntax. Any help appreciated
Avoid polluting window
Generally instead of polluting the global namespace you can store your setInterval ids in some variable
let intervalIds = {}
intervalIds['GlblThreadExe'+ID] = setInterval(function()...)
If really necessary, then store intervalIds to window
window.intervalIds = intervalIds;
Wrap your anonymous function
When you create the "clock", do not call setInterval directly:
Here, createTimerWithId will return a function which calls threadTimer
Dirty id generation
Use a timestamp, and mix it with some random stuff. Or better use a UUID
setInterval(createTimerWithId(), 1000)
function createTimerWithId(){
let id = Date.now()+Math.random(); //no lib, oneliner. good enough to debug
return function(){//the same function you gave to setInterval in your example
threadTimer(id, ...)
}
}
We can do better
In 1. we generated an id on the fly and thus
your code is not testable (id will always change(well except if you mock Math and Date...)).
your id is ugly (a float...)
it will be hard to know from which setInterval you come from
instead, give it the ID.
function createTimerWithId(ID){
return function(){//the same function you gave to setInterval in your example
threadTimer(ID, ...)
}
}
window['..'+ID] = setInterval(createTimerWithId(ID));
shorter version being
window['..'+ID] = setInterval((id=>{
return function(){
threadTimer(id, ...)
}
})(ID),1000);

Javascript: Deleting displayed objects without references using jQuery.remove()

I have objects who's only references are the DOM elements they are tied to and I'm not sure if calling $element.remove() actually removes the reference or just the DOM element. Here's some example code.
var Foo = function() {
var constructor = function Foo() {
var col = '#'+(Math.random()*0xFFFFFF<<0).toString(16);
var $container = $('<div class="element" style="background-color:'+col+';"></div>');
var $remove = $('<input type="button" value="Delete" />');
$container.append($remove);
$('#wrapper').append($container);
$remove.on('click', function() {
$container.remove();
});
};
return constructor;
}();
$('#addElement').on('click', function() {
new Foo();
});
And a jsfiddle for people to play around with. Click one button to add elements, click each element's "Delete" button to remove it; from the DOM at least.
It's not necessarily a problem for my current project because of the small scale, but I think it'd be really useful to know for future reference if I'm ever working on something large.
If it doesn't, is there any way to remove the objects without holding them in some array?
EDIT: Ok, so I've kinda answered my own question using the chrome heap snapshots and adding 10,000 elements to the page. Here's the important data. (I've included HTMLInputElements because the majority of what I'm inserting are those)
Initial 10,000 Removed
Memory 2.7MB 135MB 4.0MB
Objects 1,676 81,693 11,703
InputElements 1 59,995 1
So yeah, it seems that the garbage collector is smart enough to remove the objects when the DOM element referencing them is removed. Though it's not perfect.
The objects will be garbage collected as soon as you do not have a reference to the object anymore. I'm not sure if it would be considered a "best practice" to do this though. From MDN:
On the other hand, JavaScript values are allocated when things (objects, strings, etc.) are
created and "automatically" free'd when they are not used anymore.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Memory_Management
The "automatically" described above in modern browsers means that objects that are considered "unreachable" will be marked for garbage collection.
Calling $.element.remove() will only remove the DOM element because it only knows about the DOM element. It's not possible that it could remove any other object associated with the DOM element because it does not know about that object.
http://api.jquery.com/remove/

Prototype injection, google maps api

I need to catch the event of getting back suggestions for google maps autocomplete. I know it is undocumented, but doing some research I found that it could be down via some prototype hacking.
<input type='text' id='myInput'>
<script src="http://maps.google.com/maps/api/js?libraries=places&sensor=false&language=en-EN"></script>
<script>
function catcher(key) { console.log(key); }
function MyProto() {}
MyProto.prototype = new google.maps.MVCObject();
MyProto.prototype.changed = catcher;
var gAuto = new google.maps.places.Autocomplete(
document.getElementById('myInput'), ['geocode']);
// one of two should be commented
//gAuto.__proto__.__proto__.changed = catcher; // every key, including 'predictions'
gAuto.__proto__.__proto__ = MyProto.prototype; // only 'types', '0', and 'place' when selected
</script>
JSFiddle link: http://jsfiddle.net/agentcooper/hRyTF/ (check the console)
Check the last two lines. When setting 'changed' function directly on MVCObject prototype (first one, commented), everything works great and I can catch key 'predictions' in the 'catcher' function. The problem is that catcher needs to be different if, for example, I need to have two instances of autocomplete on my page. So when I'm trying to inject custom object in autocomplete's prototype chain (last line) everything fails. Is there any way to solve this?
EDIT: working version, thanks to Sajid :-)
UPDATE: Completed the code, maybe it will be helpful to anyone
In the second line, you are replacing the entire prototype of the MVC object with an instance of the MVC object, and depending on how the thing is initialized, this will likely not work at all. The first like replaces one function, though in the process it completely breaks that function since you don't call its superclass version, so you are not extending, you are really clobbering. To not clobber, you need to do:
(function() {
var oldChanged = gAuto.__proto__.__proto__.changed;
function catcher(key) {
// call old version, and make sure to maintain this reference correctly
oldChanged.call(this, key);
// do your stuff here
}
gAuto.__proto__.__proto__.changed = catcher;
})();
One simple solution is that each object has an idea of this mentioned above. So changed has a reference to this, which will refer to the object being used as the caller's target (except in specific situations that are a bit out of scope here). But basically:
var x = new MVCObject();
x.changed('hi') // this === x
So if your two versions need to do different things, you can check which this the changed method was called from and react appropriately.

Binding jQuery click events via looping through hash grabs last element in hash?

I have a number of divs I am toggling on and off. I initially was manually binding the handlers(as per the code below), but decided to do some refactoring. However, a binding issue arose where the last key/value in the hash is the one always selected. In the example code, that would be the contact_data div. I think the problem is that the data is not being closed over, but I am not certain how to force this in JS.
var link_div = {
"#about_toggle" : "#about_stuff",
//more pairs
"#contact_toggle" : "#contact_data"
};
/*
* Before refactoring:
$("#about_toggle").click( function()
{
$("#about_stuff").toggle();
});
*/
//After
for(var key in link_div)
{
$(key).click(function()
{
alert(link_div[key]);
toggle_on_element(link_div[key]);
});
}
Actually the problem is, the data is being closed over. The anonymous function you pass in the event handler will close over your loop variable, therefore all event handlers reference the same variable. You need to invoke another function to avoid this:
for(var key in link_div)
{
$(key).click(function(k)
{
return function() {
alert(link_div[k]);
toggle_on_element(link_div[key]);
}
}(key));
}
It's all about closures, which go back to the environment frame binding model of the language. Essentially key iterates through your loop and at the end, points to the last value in the map (which by the way, may not guarantee order, im not sure of the specific implementations). As such, the anonymous function (which is shared among all the elements, because it was created once, and therefore refers to one environment frame in memory) will for all elements, toggle link_div[key], but key, for all elements, has only one value.
You can solve this by either wrapping the anonymous function to make it unique for each binding (like jAndy did), or use a naming convention to make life a little easier:
$('.togglers').click(function(){ $('#'+$(this).attr('id')+'_stuff').toggle(); });

Can't empty JS array

Yes, I am having issues with this very basic (or so it seems) thing. I am pretty new to JS and still trying to get my head around it, but I am pretty familiar with PHP and have never experienced anything like this. I just can not empty this damn array, and stuff keeps getting added to the end every time i run this.
I have no idea why, and i am starting to think that it is somehow related to the way chekbox id's are named, but i may be mistaking....
id="alias[1321-213]",
id="alias[1128-397]",
id="alias[77-5467]" and so on.
I have tried sticking
checkboxes = []; and
checkboxes.length = 0;
In every place possible. Right after the beginning of the function, at the end of the function, even outside, right before the function, but it does not help, and the only way to empty this array is to reload the page. Please tell me what I am doing wrong, or at least point me to a place where i can RTFM. I am completely out of ideas here.
function() {
var checkboxes = new Array();
checkboxes = $(':input[name="checkbox"]');
$.each(checkboxes,
function(key, value) {
console.log(value.id);
alert(value.id);
}
);
checkboxes.length = 0;
}
I have also read Mastering Javascript Arrays 3 times to make sure I am not doing something wrong, but still can't figure it out....
I think there's a lot of confusion coming out of this because you are clearing the array -- just maybe not for the purpose you want, or at the wrong time, etc.
function () {
var checkboxes = new Array(); // local-scope variable
checkboxes = $(':input[name="checkbox"]'); // new instance
$.each(checkboxes, /* ... */); // (truncated for brevity)
checkboxes.length = 0; // this clears the array
// but, for what...?
// nothing happens now
}
As per your snippet, every call to the function recreates and clears the array. However, all work with the array is done while the array is full (nothing happens after it's cleared).
Also, checkboxes is a private variable to the function. The variable only exists during execution of the function and is forgotten once the function is done.
So, what is the big picture? Why are you trying to clear the array?
To take a guess, it sounds like you're intending on clearing it for the next call of the function.
i.e. (filling in doSomething for function name):
doSomething(); // log all array elements and clears the array
doSomething(); // does nothing, since the array is already empty
To accomplish this, you need to define checkboxes in a single location outside of the function, either as a global variable or using closures (the heavily more recommended, albeit more complex, option):
NOTE: If you haven't dealt with closures before, they may not make much sense after only a single example. But, there are thousands of resources available, including Stack Overflow, to help explain them better than I can here.
// closure (or instantly-called function)
// allows for defining private variables, since they are only known by
// code blocks within this function
(function () {
// private (closure-scoped) variable(s)
var checkboxes = $(':input[name="checkbox"]');
function doSomething() {
$.each(checkboxes, /* ... */);
checkboxes.length = 0;
}
})();
The closure will run once, defining checkboxes as the array of inputs while doSomething will iterate the array before clearing it.
Now, the last step is to expose doSomething -- cause, as with checkboxes, it is also private. You can accomplish exposing it by passing the function reference from the closure to a variable outside the closure:
var doSomething = (function () {
/* ... */
return doSomething; // note that you DO NOT want parenthesis here
})();
Is setting length even possible? ;)
Any way checkboxes is a jquery object not an array.
When You do checkboxes = $(':input[name="checkbox"]'); it's not an array any more and whatever there was before has no influence. It doesn't matter what was in a variable if You assign something new to it in any language I know.
You are making some jquery related error. Please elaborate more so that I can help
Are You sure You put name="checkbox" in all of them? It doesn't seem to have a lot of sense. Maybe You waned $(':input[type="checkbox"]'); ?
Edit: that's funny. the above selector isn't too good as well. It should be:
$('input:checkbox');
as for removing stuff:
delete varname
delete arrname[elem]
is the right way to do it.
assigning null does not change the length but just makes trouble.
What about doing this:
checkboxes = new Array();
You can also delete it.
delete checkboxes;
When you rerun the function it will search for all checkboxes again.
Consider this strategy:
function() {
var checkboxes = $(':input[name=checkbox]:not(.examined)');
checkboxes.addClass('examined');
$.each(checkboxes, function(key, value) {
// ... stuff
});
}
You could also use .data() if you don't want to "pollute" the DOM.

Categories

Resources