It's actually fairly simple and a fun task to write such scripts.
Here is a long example how to turn a regular function into something like this:
I'd start with an imaginary script. I've included a scriptLoader which loads a javascript file asynchronously:
window.loadScript = function(src){
const scriptTag = document.createElement('script');
scriptTag.async = true;
scriptTag.src = src;
const anyOtherScriptTag = document.getElementsByTagName('script')[0];
anyOtherScriptTag.parentNode.insertBefore(scriptTag, anyOtherScriptTag);
}
When called like this: loadScript("/url.js")
it will insert a new script tag (before the first script tag) in the DOM and the browser will download the script.
So far so good. Let's say I want to pass this script arguments before it has loaded. Inside the script that will be loaded I access a unique global object. Let's call it window.myScriptArgs
. So ideally, once the script has loaded it reads window.myScriptArgs and executes accordingly.
Now I could do window.myScriptArgs = []
and call it a day but since my hypothetical example will only load a single script file, I add the logic to the loadScript function as well.
window.loadScript = function(src){
window.myScriptArgs = window.myScriptArgs || [];
const scriptTag = document.createElement('script');
scriptTag.async = true;
scriptTag.src = src;
const anyOtherScriptTag = document.getElementsByTagName('script')[0];
anyOtherScriptTag.parentNode.insertBefore(scriptTag, anyOtherScriptTag);
}
loadScript("/my-script.js");
Okay, so I check if myScriptArgs is already present and if not I set it to an empty array.
Now I also know that my-script.js
exposes a global myScript() method. So I write a stub for it. This stub will put every argument received to it into the myScriptArgs array:
window.myScript = () => {
window.myScriptArgs = window.myScriptArgs || [];
window.myScriptArgs.push(arguments);
}
Now I can call loadScript and immediately call myScript() with given arguments. No need to worry about loading issues or whatnot. Once "my-script.js" is loaded it reads window.myScriptArgs
and acts as excepted. The code looks like this:
window.myScript = () => {
window.myScriptArgs = window.myScriptArgs || [];
window.myScriptArgs.push(arguments);
}
window.loadScript = function(src){
window.myScriptArgs = window.myScriptArgs || [];
const scriptTag = document.createElement('script');
scriptTag.async = true;
scriptTag.src = src;
const anyOtherScriptTag = document.getElementsByTagName('script')[0];
anyOtherScriptTag.parentNode.insertBefore(scriptTag, anyOtherScriptTag);
}
loadScript("/my-script.js");
myScript('command', 'args', 'args1');
myScript('command2', 'args3', 'args4');
Okay, that works as expected. Let's optimize it. First I combine the loadScript
and myScript
stub to a single function called initMyScript():
window.initMyScript = function(src){
window.myScriptArgs = window.myScriptArgs || [];
window.myScript = window.myScript || function(){
window.myScriptArgs.push(arguments);
}
const scriptTag = document.createElement('script');
scriptTag.async = true;
scriptTag.src = src;
const anyOtherScriptTag = document.getElementsByTagName('script')[0];
anyOtherScriptTag.parentNode.insertBefore(scriptTag, anyOtherScriptTag);
}
initMyScript("/my-script.js");
myScript('command', 'args', 'args1');
myScript('command2', 'args3', 'args4');
It's nothing too fancy atm. Now I'm going to get rid of the multiple window.
calls by passing window
as an argument to initMyScript
. I'll also do this with document
.
The script looks like this:
window.initMyScript = function(p, a, src){
p.myScriptArgs = p.myScriptArgs || [];
p.myScript = p.myScript || function(){
p.myScriptArgs.push(arguments);
}
const scriptTag = a.createElement('script');
scriptTag.async = true;
scriptTag.src = src;
const anyOtherScriptTag = a.getElementsByTagName('script')[0];
anyOtherScriptTag.parentNode.insertBefore(scriptTag, anyOtherScriptTag);
}
initMyScript(window, document, "/my-script.js");
Now let's see where I repeat myself to save some more bits. I use the string script
twice, same for myScript
:
window.initMyScript = function(p, a, s, c, src){
p.myScriptArgs = p.myScriptArgs || [];
p[c] = p[c] || function(){
p.myScriptArgs.push(arguments);
}
const scriptTag = a.createElement(s);
scriptTag.async = true;
scriptTag.src = src;
const anyOtherScriptTag = a.getElementsByTagName(s)[0];
anyOtherScriptTag.parentNode.insertBefore(scriptTag, anyOtherScriptTag);
}
initMyScript(window, document, 'script', 'myScript', "/my-script.js");
Next step in my journey is to make the variables short. And I also turn this function into a self-executing function to save the window.initMyScript
definition:
(function(p, a, s, c, src){
p.myScriptArgs = p.myScriptArgs || [];
p[c] = p[c] || function(){
p.myScriptArgs.push(arguments);
}
const q = a.createElement(s);
q.async = true;
q.src = src;
const d = a.getElementsByTagName(s)[0];
d.parentNode.insertBefore(q, d);
})(window, document, 'script', 'myScript', "/my-script.js");
And to my last mystery: I edit the function parameters to confuse people and also minify the code even more. You can actually chain functions in javascript using commas ;).
(function(p, a, s, c, A, l, i){
p["myScriptArgs"]=p["myScriptArgs"]||[],p[c] = p[c]||function(){
p["myScriptArgs"].push(arguments)},
l = a.createElement(s);l.async = true;l[A] = A;
i = a.getElementsByTagName(s)[0];
i.parentNode.insertBefore(l, i);
})(window, document, 'script', 'myScript', "/my-script.js");
myScript("arg1", "arg2");
myScript("arg2", "arg3");
Do note that I add two extra parameters in the function, that's because I need to save the element returned by createElement
and don't want to use a var
statement ;).
You can take this even further, but you get the point. For small functions, you can do it yourself without a problem.
Furthermore, you can use a minifier like UglifyJS and then rename the variables yourself afterward if you are really into that whole isogram thing...
Note: I didn't test any of this code. Here be dragons.
The imaginary code is my bad attempt at de-obfuscating googles example. The google-analytics snippet works almost the same as my custom snippet. GA optimizes a bit more (e.g. turning true into 1) but you get the point.
Read more about the things used in my example:
Immediately Invoked Function Expression
Property accessors (especially Bracket notation)
And javascript specific things like passing three arguments to a function which takes 5.