Rolling my own Require - javascript

Forward
It has come to my attention that this problem was a "Function Scoping - Not Block Scoping" answer, the difference being for ... in opposed to for ... i<n ... i++ The solution could be wrapping the for(var p in ...) { with a function to give them their own scope (much like was done with Array.prototype.forEach). Thank you for your help.
Problem
I'm rolling my own tiny Require.js solution as opposed to the actual Require.js library (no I haven't taken a look at their source).
My callback function never seems to be executed, but I can't figure out why. It's probably a simple logical error, but you know how it goes when you stare at your own code for too long. (Everything looks logically sound)
Usage
Used as follows:
require(["LibraryA", "LibraryB", "LibraryC"], function() {
//code which requires libraries a-c here
});
Code
var require = (function() {
var scriptsLoaded = [];
return function(paths, callback) {
var pathsDoneLoading = {};
for(var i = 0; i < paths.length; i++) {
pathsDoneLoading[paths[i]] = false;
}
for(var p in pathsDoneLoading) {
for(var i = 0; i < scriptsLoaded.length; i++) {
if(p === scriptsLoaded[i]) {
pathsDoneLoading[p] = true;
}
}
}
for(var p in pathsDoneLoading) {
if(pathsDoneLoading[p] === false) {
var script = document.createElement("script");
script.src = p + ".js";
script.onload = function() {
scriptsLoaded.push(p);
pathsDoneLoading[p] = true;
for(var p in pathsDoneLoading) {
if(pathsDoneLoading[p] !== true) {
return;
}
}
callback();
};
document.documentElement.appendChild(script);
}
}
}
})();
Plunker
https://plnkr.co/edit/OFWTUpmV3sIJAjhGhcLO

It's the old for loop variable catching problem.
During your loop where it's iterating through what paths are loading (for (var p in pathsDoneLoading) {) the p is being bound to the function inside. The problem is that p changes and persists after the loop has complete so every single function is adding the last key in the pathsDoneLoading object to the scriptsLoaded array (and setting the matching value in pathsDoneLoading to true).
An example of the problem:
var obj = { a: 1, b: 2, c: 3 };
var funcs = [];
for (var key in obj) {
funcs.push(function() {
return key; // Remember, `key` is bound to the function, not the block
});
}
funcs.forEach(function(f) {
document.querySelector('pre').innerText += f() + '\n';
});
<pre></pre>
To fix this, you can wrap it in an IIFE to create a new binding per p value.
for (var p in pathsDoneLoading) {
(function(p) {
if (pathsDoneLoading[p] === false) { // or `if (!pathsDoneLoading[p])`
...
})(p);
}
That way every time you generate an onload handler, they have a unique binding to the intended path.
Our previous example fixed with this solution:
var obj = { a: 1, b: 2, c: 3 };
var funcs = [];
for (var key in obj) {
(function(key) {
funcs.push(function() {
return key; // Remember, `key` is bound to the function, not the block
});
})(key);
}
funcs.forEach(function(f) {
document.querySelector('pre').innerText += f() + '\n';
});
<pre></pre>

Simply put, you'll need capture the for loop iterator because it's a moving target and by the time onload fires, it will have changed.
var require = (function() {
var scriptsLoaded = [];
return function(paths, callback) {
var pathsDoneLoading = {};
for(var i = 0; i < paths.length; i++) {
pathsDoneLoading[paths[i]] = false;
}
for(var p in pathsDoneLoading) {
for(var i = 0; i < scriptsLoaded.length; i++) {
if(p === scriptsLoaded[i]) {
pathsDoneLoading[p] = true;
}
}
}
for(var p in pathsDoneLoading) {
if(pathsDoneLoading[p] === false) {
var script = document.createElement("script");
script.src = p + ".js";
// Wrapping in a closure
script.onload = (function(Vp) {
scriptsLoaded.push(Vp);
pathsDoneLoading[Vp] = true;
for(var prop in pathsDoneLoading) {
if(pathsDoneLoading[prop] !== true) {
return;
}
}
callback();
// Execute the closure function and send in your
// var(s) as argument(s) to capture it's current state
}(p));
document.documentElement.appendChild(script);
}
}
}
})();

Related

JavaScript remove an IIFE event listener

I'm trying to remove click events from a list of id's after adding them with an IIFE like this
function setupPlayer(player){
var squareState = {};
for (i = 0; i < allSquares.length; i++) {
if(allSquares[i].innerHTML === "") {
// set up a click event for each square
document.getElementById(allSquares[i].getAttribute('id')).addEventListener('click', (clickSquare)(i));
}
}
}
The clickSquare function returns
function clickSquare(i){
var num = i;
return function() {
document.getElementById(allSquares[num].getAttribute('id')).innerHTML=player;
}
}
Then I try to remove them with
function removeClickEvents(){
for (let i = 0; i < allSquares.length; i++) {
document.getElementById(allSquares[i].getAttribute('id')).removeEventListener('click', clickSquare);
}
}
I've tried naming the returned anonymous function and using removeEventListener on that to no avail.
To remove event listener from a DOM element you need to pass the same function you used while adding event listener, as the parameter.
In javascript when you create an object it creates a new instance of that object class, so it won't be equal to another object even if it is created with same parameters
Example:
{} != {} // returns true
[] != [] // returns true
Same goes with function, whenever you write function (){} it creates a new instance of Function class.
Example:
function a() {
return function b() {}
}
a() != a() // returns true
Solution:
So for you to be able to remove the event listeners, you will have to store the functions you have passed to addEventListener
var listeners = [];
function setupPlayer(player) {
var squareState = {};
for (i = 0; i < allSquares.length; i++) {
if(allSquares[i].innerHTML === "") {
listeners[i] = clickSquare(i);
document.getElementById(allSquares[i].getAttribute('id')).addEventListener('click', listeners[i]);
}
}
}
function clickSquare(i) {
var num = i;
return function() {
document.getElementById(allSquares[num].getAttribute('id')).innerHTML=player;
}
}
function removeClickEvents() {
for (let i = 0; i < allSquares.length; i++) {
if(listeners[i]) {
document.getElementById(allSquares[i].getAttribute('id')).removeEventListener('click', listeners[i]);
}
}
}
From your code where you are using
document.getElementById(allSquares[i].getAttribute('id'))
I am assuming that allSquares[i] is a DOM element already, your code can be more simplified
var listeners = [];
function setupPlayer(player) {
var squareState = {};
for (i = 0; i < allSquares.length; i++) {
if(allSquares[i].innerHTML === "") {
listeners[i] = clickSquare(i);
allSquares[i].addEventListener('click', listeners[i]);
}
}
}
function clickSquare(i) {
var num = i;
return function() {
allSquares[num].innerHTML=player;
}
}
function removeClickEvents() {
for (let i = 0; i < allSquares.length; i++) {
if(listeners[i]) {
allSquares[i].removeEventListener('click', listeners[i]);
}
}
}
The function is being called immediately at (clickSquare)(i). At code at Question allSquares appears to be the element itself, clickSquare function can be referenced directly and event.target can be used within event handler to reference the current element in allSquares collection
let player = 123;
setInterval(() => player = Math.random(), 1000);
onload = () => {
let allSquares = document.querySelectorAll("div[id|=square]");
let button = document.querySelector("button");
button.onclick = removeClickEvents;
function setupPlayer(player) {
var squareState = {};
for (let i = 0; i < allSquares.length; i++) {
if (allSquares[i].innerHTML === "click") {
// set up a click event for each square
allSquares[i].addEventListener('click', clickSquare);
}
}
}
function clickSquare(event) {
console.log(event.target);
event.target.innerHTML = player;
}
function removeClickEvents() {
for (let i = 0; i < allSquares.length; i++) {
allSquares[i].removeEventListener('click', clickSquare);
}
}
setupPlayer(player);
}
<div id="square-0">click</div>
<div id="square-1">click</div>
<div id="square-2">click</div>
<button>remove events</button>

Binding event listeners multiple times in JavaScript

I'm trying to get the directory list, level by level, using JavaScript.
I have this paths array as input.
var _paths = [];
_paths.push("meta1/meta2/test/home/myself/hi.jpg");
_paths.push("meta1/meta2/test/home/myself/hi1.jpg");
_paths.push("meta1/meta2/test/home/myself/hi2.jpg");
_paths.push("meta1/meta2/test/work/she/100.jpg");
_paths.push("meta1/meta2/test/work/she/110.jpg");
_paths.push("meta1/meta2/test/work/she/120.jpg");
_paths.push("meta1/meta2/test/work/hard/soft/she/120.jpg");
_paths.push("meta1/meta2/test/work/hard/soft/she/121.jpg");
_paths.push("meta1/meta2/test/work/she/220.jpg");
and I want to have a "test" as output, which will be clickable. After click "test", it should be replaced by "home" and "work". After click on "home" - "myself", on "work" - "hard" and "she".
I wrote this:
CodepenCode
and it works only once, only when clicking on "test".
Simply rebind the listeners after the directories have been drawn. You bind them only once, thus they work only once.
Wrap the binding function into a named function:
function bindListeners(){
$('.sub').click(function() {
word = $(this).text();
filteredArr = findString(_paths, word);
drawList(filteredArr, word);
});
}
And call it at the end of drawList:
var drawList = function (paths, word) {
var folders = getFolders(paths, word);
if (folders.length > 0) {
$('.canvas').html('');
for (i = 0; i < folders.length; i++) {
$('.canvas').append("<div class='sub'>" + folders[i] + "</div><br />");
}
}
bindListeners();
}
Demo.
If anyone is curious about building out the data structure:
(function iteratePaths() {
var dirs = [];
for(var i = 0; i < _paths.length; i++) {
buildDirectories(dirs, _paths[i].split('/'));
}
})();
function findDir(dir, obj) {
for(var i = 0; i < dir.length; i++) {
if(dir[i].name === obj.name) {
return dir[i];
}
}
return undefined;
}
function buildDirectories(dir, subs) {
if(subs.length === 0) return;
var obj = {name: subs.shift(), dirs: []};
var existingDir = findDir(dir, obj);
if(!existingDir) {
dir.push(obj);
}
buildDirectories((existingDir || obj).dirs, subs);
}

Get object caller name by function call JavaScript

I'm writing a piece of code to easily save error logs in an object for debugging.
What I'm trying to achieve is to get the Object name from the function it was called from like so:
var MainObject = {
test : function() {
return MainObject.test.caller;
// When called from MainObject.testcaller,
// it should return MainObject.testcaller.
},
testcaller : function() {
return MainObject.test(); // Should return MainObject.testcaller, Returns own function code.
},
anothercaller : function() {
return MainObject.test(); // Should return MainObject.anothercaller, Returns own function code.
}
}
However when I run this code it returns the function code from MainObject.testcaller.
JSFiddle example
Is there any way this is possible?
Update
After looking at Rhumborl's answer, I discovered that assigning the value through another function would lead it to point back at the function name without the object itself.
Code:
(function (name, func) {
MainObject[name] = func;
})('invalid', function() {
return MainObject.test("blah");
});
// This now points at invalid() rather than MainObject.invalid()
Updated fiddle
There is a non–standard caller property of functions that returns the caller function, however that is a pointer to a function object and doesn't tell you the object it was called as a method of, or the object's name. You can get a reference to the function through arguments.callee.
There is also the obsolete arguments.caller, but don't use that. It also provides a reference to the calling function (where supported).
Once you have a reference to the calling function (if there is one), you then have the issue of resolving its name. Given that Functions are Objects, and objects can be referenced by multiple properties and variables, the concept of a function having a particular name is alluvial.
However, if you know that the function is a property of some object, you can iterate over the object's own enumerable properties to find out which one it is.
But that seems to be a rather odd thing to do. What are you actually trying to do? You may be trying to solve a problem that can be worked around in a much more robust and simpler way.
Edit
You can do what you want in a very limited way using the method described above for the case in the OP, however it is not robust or a general solution:
var mainObject = {
test : function() {
var obj = this;
var caller = arguments.callee.caller;
var global = (function(){return this}());
var fnName, objName;
for (var p in global) {
if (global[p] === obj) {
objName = p;
}
}
for (var f in obj) {
if (obj[f] === caller) {
fnName = f;
}
}
return objName + '.' + fnName;
},
testcaller : function() {
return mainObject.test();
},
anothercaller : function() {
return mainObject.test();
}
}
console.log(mainObject.testcaller()); // mainObject.testcaller
console.log(mainObject.anothercaller()); // mainObject.anothercaller
but it's brittle:
var a = mainObject.anothercaller;
console.log(a()); // mainObject.anothercaller
var b = {
foo : mainObject.anothercaller
}
console.log(b.foo()); // mainObject.anothercaller
Oops.
You can use this trick at http://www.eriwen.com/javascript/js-stack-trace/ which throws an error, then parses the stack trace.
I have updated it for the latest versions of Firefox, Chrome and IE. Unfortunately it doesn't work well on my IE9 (and I haven't tested it on Opera).
function getStackTrace() {
var callstack = [];
var isCallstackPopulated = false;
try {
i.dont.exist += 0; //doesn't exist- that's the point
} catch (e) {
if (e.stack) { //Firefox/Chrome/IE11
var lines = e.stack.split('\n');
for (var i = 0, len = lines.length; i < len; i++) {
var line = lines[i].trim();
if (line.match(/^at [A-Za-z0-9\.\-_\$]+\s*\(/)) {
// Chrome/IE: " at Object.MainObject.testcaller (url:line:char)"
var entry = line.substring(3, line.indexOf('(') - 1);
// Chrome appends "Object." to the front of the object functions, so strip it off
if (entry.indexOf("Object.") == 0) {
entry = entry.substr(7, entry.length);
}
callstack.push(entry);
} else if (line.match(/^[A-Za-z0-9\.\-_\$]+\s*#/)) {
// Firefox: "MainObject.testcaller#url:line:char"
callstack.push(line.substring(0, lines[i].indexOf('#')));
}
}
//Remove call to getStackTrace()
callstack.shift();
isCallstackPopulated = true;
} else if (window.opera && e.message) { //Opera
var lines = e.message.split('\n');
for (var i = 0, len = lines.length; i < len; i++) {
if (lines[i].match(/^\s*[A-Za-z0-9\-_\$]+\(/)) {
var entry = lines[i];
//Append next line also since it has the file info
if (lines[i + 1]) {
entry += lines[i + 1];
i++;
}
callstack.push(entry);
}
}
//Remove call to getStackTrace()
callstack.shift();
isCallstackPopulated = true;
}
}
if (!isCallstackPopulated) { //IE9 and Safari
var currentFunction = arguments.callee.caller;
while (currentFunction) {
var fn = currentFunction.toString();
var fname = fn.substring(fn.indexOf("function") + 8, fn.indexOf('')) || 'anonymous';
callstack.push(fname);
currentFunction = currentFunction.caller;
}
}
return callstack;
}
var MainObject = {
test: function (x) {
// first entry is the current function (test), second entry is the caller
var stackTrace = getStackTrace();
var caller = stackTrace[1];
return caller + "()";
},
testcaller: function () {
return MainObject.test(1, null);
}
}
function SomeFunction() {
return MainObject.test("blah");
}
document.body.innerHTML += '<b style="color: red">' + MainObject.testcaller() + '</b>';
document.body.innerHTML += '<div>Calling SomeFunction() returns: <b style="color: red">' + SomeFunction() + '</b></div>';
MainObject.test() should return: <b style="color: blue">MainObject.testcaller()</b>
<hr />
MainObject.test() returns:
Updated fiddle here

Mutable variable is accessible from closure. How can I fix this?

I am using Typeahead by twitter. I am running into this warning from Intellij. This is causing the "window.location.href" for each link to be the last item in my list of items.
How can I fix my code?
Below is my code:
AutoSuggest.prototype.config = function () {
var me = this;
var comp, options;
var gotoUrl = "/{0}/{1}";
var imgurl = '<img src="/icon/{0}.gif"/>';
var target;
for (var i = 0; i < me.targets.length; i++) {
target = me.targets[i];
if ($("#" + target.inputId).length != 0) {
options = {
source: function (query, process) { // where to get the data
process(me.results);
},
// set max results to display
items: 10,
matcher: function (item) { // how to make sure the result select is correct/matching
// we check the query against the ticker then the company name
comp = me.map[item];
var symbol = comp.s.toLowerCase();
return (this.query.trim().toLowerCase() == symbol.substring(0, 1) ||
comp.c.toLowerCase().indexOf(this.query.trim().toLowerCase()) != -1);
},
highlighter: function (item) { // how to show the data
comp = me.map[item];
if (typeof comp === 'undefined') {
return "<span>No Match Found.</span>";
}
if (comp.t == 0) {
imgurl = comp.v;
} else if (comp.t == -1) {
imgurl = me.format(imgurl, "empty");
} else {
imgurl = me.format(imgurl, comp.t);
}
return "\n<span id='compVenue'>" + imgurl + "</span>" +
"\n<span id='compSymbol'><b>" + comp.s + "</b></span>" +
"\n<span id='compName'>" + comp.c + "</span>";
},
sorter: function (items) { // sort our results
if (items.length == 0) {
items.push(Object());
}
return items;
},
// the problem starts here when i start using target inside the functions
updater: function (item) { // what to do when item is selected
comp = me.map[item];
if (typeof comp === 'undefined') {
return this.query;
}
window.location.href = me.format(gotoUrl, comp.s, target.destination);
return item;
}
};
$("#" + target.inputId).typeahead(options);
// lastly, set up the functions for the buttons
$("#" + target.buttonId).click(function () {
window.location.href = me.format(gotoUrl, $("#" + target.inputId).val(), target.destination);
});
}
}
};
With #cdhowie's help, some more code:
i will update the updater and also the href for the click()
updater: (function (inner_target) { // what to do when item is selected
return function (item) {
comp = me.map[item];
if (typeof comp === 'undefined') {
return this.query;
}
window.location.href = me.format(gotoUrl, comp.s, inner_target.destination);
return item;
}}(target))};
I liked the paragraph Closures Inside Loops from Javascript Garden
It explains three ways of doing it.
The wrong way of using a closure inside a loop
for(var i = 0; i < 10; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
Solution 1 with anonymous wrapper
for(var i = 0; i < 10; i++) {
(function(e) {
setTimeout(function() {
console.log(e);
}, 1000);
})(i);
}
Solution 2 - returning a function from a closure
for(var i = 0; i < 10; i++) {
setTimeout((function(e) {
return function() {
console.log(e);
}
})(i), 1000)
}
Solution 3, my favorite, where I think I finally understood bind - yaay! bind FTW!
for(var i = 0; i < 10; i++) {
setTimeout(console.log.bind(console, i), 1000);
}
I highly recommend Javascript garden - it showed me this and many more Javascript quirks (and made me like JS even more).
p.s. if your brain didn't melt you haven't had enough Javascript that day.
You need to nest two functions here, creating a new closure that captures the value of the variable (instead of the variable itself) at the moment the closure is created. You can do this using arguments to an immediately-invoked outer function. Replace this expression:
function (item) { // what to do when item is selected
comp = me.map[item];
if (typeof comp === 'undefined') {
return this.query;
}
window.location.href = me.format(gotoUrl, comp.s, target.destination);
return item;
}
With this:
(function (inner_target) {
return function (item) { // what to do when item is selected
comp = me.map[item];
if (typeof comp === 'undefined') {
return this.query;
}
window.location.href = me.format(gotoUrl, comp.s, inner_target.destination);
return item;
}
}(target))
Note that we pass target into the outer function, which becomes the argument inner_target, effectively capturing the value of target at the moment the outer function is called. The outer function returns an inner function, which uses inner_target instead of target, and inner_target will not change.
(Note that you can rename inner_target to target and you will be okay -- the closest target will be used, which would be the function parameter. However, having two variables with the same name in such a tight scope could be very confusing and so I have named them differently in my example so that you can see what's going on.)
In ecmascript 6 we have new opportunities.
The let statement declares a block scope local variable, optionally initializing it to a value.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/let
Since the only scoping that JavaScript has is function scope, you can simply move the closure to an external function, outside of the scope you're in.
Just to clarify on #BogdanRuzhitskiy answer (as I couldn't figure out how to add the code in a comment), the idea with using let is to create a local variable inside the for block:
for(var i = 0; i < 10; i++) {
let captureI = i;
setTimeout(function() {
console.log(captureI);
}, 1000);
}
This will work in pretty much any modern browser except IE11.

When I make a clone of the object shows me an error message

First the following is the code of my own javascript library.
(function() {
var lib = {
elems: [],
getElem: function() {
var tmpElem = [];
for (var i = 0; i < arguments.length; i++)
tmpElem.push(document.getElementById(arguments[i]));
this.elems = tmpElem;
tmpElem = null;
return this;
},
html: function(txt) {
for (var i = 0; i < this.elems.length; i++)
this.elems[i].innerHTML = txt;
return this;
},
style: function(prob, val) {
for (var i = 0; i < this.elems.length; i++)
this.elems[i].style[prob] = val;
return this;
},
addEvent: function(event, callback) {
if (this.elems[0].addEventListener) {
for (var i = 0; i < this.elems.length; i++)
this.elems[i].addEventListener(event, callback, false);
} else if (this.elems[0].attachEvent) {
for (var i = 0; i < this.elems.length; i++)
this.elems[i].attachEvent('on' + event, callback);
}
return this;
},
toggle: function() {
for (var i = 0; i < this.elems.length; i++)
this.elems[i].style.display = (this.elems[i].style.display === 'none' || '') ? 'block' : 'none';
return this;
},
domLoad: function(callback) {
var isLoaded = false;
var checkLoaded = setInterval(function() {
if (document.body && document.getElementById)
isLoaded = true;
}, 10);
var Loaded = setInterval(function() {
if (isLoaded) {
clearInterval(checkLoaded);
clearInterval(Loaded);
callback();
}
}, 10);
}
};
var fn = lib.getElem;
for(var i in lib)
fn[i] = lib[i];
window.lib = window.$ = fn;
})();
Previously, I have used this way to use my own library, and works fine .
$.getElem('box').html('Welcome to my computer.');
But when updated the code of my own library, and I added
var fn = lib.getElem;
for(var i in lib)
fn[i] = lib[i];
To be using the element selector like this way
$('box').html('Welcome to my computer.');
But the problem began appear when added the updated code to clone the lib object TypeError: $(...).html is not a function.
And now I want to use the element selector like that
$('box').html('Welcome to my computer.');
instead of
$.getElem('box').html('Welcome to my computer.');
You create a variable fn which has a reference to "getElem" but since fn is not a property on your lib object then it means that when getElem refers to "this" it will be you global object which is propbably window.
Remove all the following 3 lines
var fn = lib.getElem;
for(var i in lib)
fn[i] = lib[i];
and then do this
window.$ = function () { return lib.getElem.apply(lib, arguments); };
This will allow getElem to be called as $ but maintaining "lib" as context.
Although I don't know exactly what you are trying to achieve with those additional lines, just by reading the code, lib.getElem does not have a function called html
lib does.
Hence, just var fn = lib; should do just fine.
There more ways to achieve this but the root cause is in your getElem() function: return this;
$ is a reference to that function. If you call $() it is called as a function and not as a method. Therefore this refers to window and window has, of course, no html() function.
You could do return lib; to fix the problem.

Categories