I’ve been talking about functional JavaScript for a few posts, but, to be honest, it’s nice to put the theory aside and just practice thinking and writing functionally. With that in mind, let see what we can do about fixing some “copy-n-paste” code.
I bought a theme for this site a month or so back – you’re looking at it. As part of the theme, you get some HTML showing what various types of pages look like, the CSS to render it all, and some JavaScript. Usually the HTML/CSS is fine, but then I take a look at the JavaScript and I wonder.
As an example, here’s some code that was part of this theme. It manages the search textbox “popup”.
$('.search > .icon-search').click(function () {
$('.search_popup').slideDown('', function () { });
$('.search > .icon-search').toggleClass('active');
$('.search > .icon-remove').toggleClass('active');
});
$('.search > .icon-remove').click(function () {
$('.search_popup').slideUp('', function () { });
$('.search > .icon-search').toggleClass('active');
$('.search > .icon-remove').toggleClass('active');
});
Me, I look at this, and say, talk about code-duplication. It seems to be crying out for some variables to hold the results of calling jQuery on those complex repeated selector expressions. However, both of these calls are IIFEs (Immediately Invoked Function Expressions), so where are those “local” variables to be kept? (And what about those “empty” callbacks? WTF?)
But then I do recognize that the code is reasonably descriptive in and of itself. What can be done? Easily?
Time, I thought to myself, for a memoizer to wrap jQuery. The effect I wanted was this, in essence:
var memoizeJQ = function (fn) {
// code that wraps fn
};
var jq = memoizeJQ(jQuery);
jq('.search > .icon-search').click(function(){
jq('.search_popup').slideDown();
jq('.search > .icon-search').toggleClass('active');
jq('.search > .icon-remove').toggleClass('active');
});
jq('.search > .icon-remove').click(function(){
jq('.search_popup').slideUp();
jq('.search > .icon-search').toggleClass('active');
jq('.search > .icon-remove').toggleClass('active');
});
So we memoize the jQuery function and then just keep calling the memoized function instead. Internally, it will cache the results of applying jQuery on different selector expressions, and return the cached version as and when necessary. The plan was to alleviate the time taken in calculating the returned jQuery object for those complex repeated selector expressions. Here’s the memoize function in more detail:
var memoizeJQ = function (fn) {
"use strict";
var cache = {};
return function () {
var args = Array.prototype.slice.call(arguments);
var first = args.length > 0 ? args[0] : undefined;
if (args.length === 1) {
if (!cache[first]) {
cache[first] = fn.call(this, first);
}
return cache[first];
}
return fn.apply(this, args);
};
};
So first I declare a cache as an empty hashmap within the closure. (Yes, OK, it’s an empty object, but a JavaScript object is nothing more or less than a hashmap or dictionary.) After that I return the memoized function. This function will convert the arguments “array” to a real array and extract the first parameter from it. If there was only one argument, we check the cache for that first parameter, if it’s not been set, we call the original function and cache (and return) the result. If the arguments array is not of length 1, we just call the original function and return the result.
After I’d written this, I started to worry a little about immutability: this code assumes that calling jQuery on a selector expression the first time and calling it on the same expression much later, after, perhaps, many changes to the DOM have been made, will produce the same result. The answer sometimes is yes, and then sometimes it’s no. For my original scenario, sure, the answer I knew was yes, but in general that may not be the case. We need a way to reset the internal cache in those kinds of situations.
I decided to add the option of two other formulations to help in this reset:
jq("reset");
jq("reset", selector);
The first should reset the entire cache. All cached results should be thrown away, so that the next call to the memoized function would call the wrapped function. The second would just reset the cache for the passed selector expression, and call the wrapped function for it (and cache the result again). Note that "reset
is not a valid selector for jQuery (it would be interpreted as an element selector, and there is no HTML element called "
reset
).
Here’s the final code for the memoizer that covers this new requirement:
var memoizeJQ = function (fn) {
"use strict";
var cache = {};
return function () {
var args = Array.prototype.slice.call(arguments);
var first = args.length > 0 ? args[0] : undefined;
if (args.length === 1) {
if (first === "reset") {
cache = {};
return null;
}
if (!cache[first]) {
cache[first] = fn.call(this, first);
}
return cache[first];
}
if ((args.length === 2) && (first === "reset")) {
first = args[1];
cache[first] = fn.call(this, first);
return cache[first];
}
return fn.apply(this, args);
};
};
As I said, a fun interlude showing how to wrap an existing function to give it some extra functionality – in this case, caching the results. But it does help reinforce the fact that JavaScript supports higher-order functions, or that functions are objects that can be passed around.
UPDATE: for fun, I “linified” the banner photo of our cat. You can play around with this yourself at linify.com.
2 Responses
#1 Mike McNally said...
17-Feb-16 9:01 AMWhere exactly are the IIFEs? Your initial jQuery code passes some anonymous functions into jQuery to set up the handlers, but the functions aren't invoked. An IIFE is a function expression that's invoked as part of the containing expression. That doesn't happen in any of the code in this post.
#2 julian m bucknall said...
17-Feb-16 10:06 AM@Mike: Yes, you are correct and I was enthusiastic (but not thinking clearly at the time) in writing what I wrote. Quite right: there are no IIFEs there :(. What I meant was that these calls are executed immediately; they weren't wrapped in, say, a jQuery document ready function. [Makes note to really read what I write before posting.]
Leave a response
Note: some MarkDown is allowed, but HTML is not. Expand to show what's available.
_emphasis_
**strong**
[text](url)
`IEnumerable`
* an item
1. an item
> Now is the time...
Preview of response