There are plenty of warnings out there that reliance on undocumented behavior is dangerous and could potentially break your app. Of course, there are many more developers out there that will still manage to sneak in a hack every now and then that will take advantage of some quirk which may or may not come back to haunt them later. The latest one of these I found had to do with jQuery’s
text() function and its behavior across versions.
While refactoring a portion of our site we upgraded from jQuery 1.6.4 to 1.11.0. During QA it was reported that there was some random vertical spacing appearing between some text that was not there prior to the upgrade.
Great, one of these. I opened up my dev tools to see what was going on.
Without changing any script related to error message construction, a blank div appeared out of the nether that wasn’t there before. I dug deeper into the third-party script we fetched server-side that dealt with these messages and found the relevant code:
So if any one of the
year fields were blank, the code would show an error message for the date of birth field. On a subsequent iteration, if it caught another blank instance of those three fields, it defaulted to appending an empty error div and assigning a default message for that particular field.
year were not indices of
self._blankMessages, so the argument defaulted to
undefined. On the old page, the result was no div being constructed. Why did it start appearing after the upgrade?
My suspicions were confirmed after running the same code on a test page with two different jQ libraries:
The same method returned a different value for each version of jQuery. Let’s take a closer look at the source code:
Both versions check if the value is defined in order to determine if it should invoke getter or setter behavior. However 1.11.0 makes use of a separate function called
access to do this. This function appeared sometime between 1.6.4 and 1.7.2.
So what’s changed? In
1.6.4 an undefined argument triggered the getter functionality of
.text(), which was actually an issue of some debate years ago. With the addition of the access function however, a new condition chainable was introduced. In a typical getter invocation,
.text() will call access as follows:
arguments.length is 0, chainable would evaluate to false and the callback provided from text, which switches on
return value === undefined and runs
jQuery.text( this ), would be invoked via
fn.call( elems ).
According to the issue above, passing
.text(undefined) should invoke the getter as well – except we never get there. Since passing actual undefined results in
arguments.length evaluating to 1, even though value is the same in both scenarios the callback function is never invoked! Instead, elems is returned rather than the blank string from the result of
jQuery.text, so that was why the blank div was being generated in the new site.
I’m not sure if this modified behavior was an intended side effect of jQuery’s
access function, but there is a more important lesson here. The code should have been more explicit in handling this kind of scenario, especially since the fix is very simple:
Problem solved. Instead of defaulting to the
else case when another DOB field is encountered, simply skip adding a DOB error div if we already did so. Relying on a side-effect of passing undefined is never a good idea, and it will eventually create a headache for your coworkers.
I understand this kind of stuff happens — after all, deadlines exist — but I hope this will remind you (it certainly did for me) to stop and think for a second next time you’re in a situation like this and evaluate whether an odd behavior may be subject to change in the future.