Let's make it clear. I don't pretend to challenge a tradition so well established. Descriptive names are essential for improving readability and maintaining your code. And this is especially crucial for classes, modules, functions, methods, and properties. Defining and applying good naming conventions is the core of successful frameworks. Names reveal how designers have succeeded in abstracting objects and factorizing tasks. So, really, I will not advocate cryptic coding!

The “Naked Process”

However, the more I write modular code, the more I feel that those descriptive variable names (userName, currentWordCount etc.) are really not what I have to focus on. In fact, they tend to hide me the underlying design pattern. It is of no importance that this string refers to a user name since my function only wants to split it according to some separator. It is of no importance that this array addresses swatches or potatoes since I just need to remove duplicates.

I admit that my examples are quite artificial. Obviously, if I am in writing a split function, I have already gone through the abstraction stage and I have no reason to qualify the argument as a user name anymore. My point is that designing pure functions—whatever purity may mean to you—is like removing the context in a way that we finally see the naked process. And, on this particular side of the art, I think variables and arguments should be as transparent as possible.

Consider a basic routine that deletes all the properties of the incoming object. The recommended code in ExtendScript would probably look like

const deleteProperties = function(myObj)
//----------------------------------
{
    var prop;
 
    for( prop in myObj )
    {
        if( myObj.hasOwnProperty(prop) )
        {
            delete myObj[prop];
        }
    }
};
 

Which is fine and agnostic enough. But for my taste, those uninteresting names pollute the landscape. So I use minimalist symbols instantly understandable to me:

const deleteProperties = function(/*obj&*/o,  k)
//----------------------------------
{
    for( k in o )
    {
        o.hasOwnProperty(k) && delete o[k];
    }
};
 

I just want to see what actually happens, I don't want my eyes to waste time on arbitrary names.

(The meaning of /*obj&*/o, k will be explained soon.)

Alphabet

Of course this is in no way standard, and there are probably many reasons to denounce such coding style. But it has gradually become a routine. I have now a quite stable set of letters—and a few bigrams—that express the basic data types in use in generic functions. I know that any i is an iterating integer, any o represents the current object (in terms of key-value pairing structure) while any a refers to the current array (in terms of index-value pairing structure) etc.

Here is my personal list of pre-typed variable names:

NAME EXAMPLE PURPOSE
a a = [1,2,3] Array, or any structure which one can loop in by indices.
b b = !!a.length Usually: Boolean (true/false), or boolean-like (0/1), or bit value. Specially: alt. array (when a vs. b makes sense.)
c c = s[i] Character, that is, single character string.
cc cc = s.charCodeAt(i) Character code (UTF16 uint.)
d d = j-i Usually: offset, distance, difference. As a prefix: dx, dy, etc. Specially: Date object.
e try{…} catch(e){…} Error (object or string) when some treatment is done in accordance. Otherwise I just use a mute underscore.
ev ev = new UIEvent('clicking',true,true) Event.
f f = String.fromCharCode Function, that is, callable object. Mostly used for naming a callback argument.
F var myFunc=function F(){…} Inner name of the current function, in particular when recursive calls are needed.
ff ff = new File(s) File object.
fd fd = Folder.appPackage Folder object.
fx ff = new File(''+fx) Usually: hybrid type that refers to either a File object or a File path (string.). Specially: File or Folder.
h h = y1-y0 Height (number.)
i for( i=0 ; i<n ; ++i ){…} Iterating index.
j for( j=i ; j<n ; ++j ){…} Iterating index (sub-level.)
k for( k in o ){…} Key (name of a property or method.)
l DO NOT USE EVER! (‘l’ vs. ‘1’ readability issue.)
m m = s.match(re) Array of matches.
mx mx = o.transformValuesOf(…)[0] Matrix.
n n = a.length Total count or size (uint.) I usually use n (sometimes N) to keep track of a fixed size. When the size is changing during the process, z is preferred.
o o = {a:1, b:2, c:3} Object (set of key-value pairs.)
oo oo = o[k] Sub-object (object inside an object.)
p p = s.indexOf(ss) Some position (string or array index.)
q if( q.hasOwnProperty(k) ) return q[k] Special object used as a cache.
r r = a.length ? a : null Usually: Name of the returned value (function result.) Specially: Reference (an external object that the function is modifying.)
re re = /abc.+/g RegExp.
s s = "foobar" String.
ss ss = s.substr(0,p) Usually: Sub-string. Specially: settings (a set of parameters.)
t t = x<y ? w : h Any temporary value (of any type.)
u u = 0x10AAAA Usually: undefined variable. Specially: Unicode code point (uint24.)
v v = parseFloat(s) Any scalar value.
w w = x1-x0 Usually: width (number). Specially: weight, or Window object.
x x = o.geometricBounds[1] Usually: X-coordinate, or any numeric value in a x vs. y context. Specially: argument of undetermined type (variant.)
y y = o.geometricBounds[0] Y-coordinate, or any numeric value in a x vs. y context.
xy xy = [x,y] Array of 2 coordinates.
z a[z++] = t Usually: growing count (by contrast with n.) Specially: Z-coordinate (depth) or complex point (alias of xy.)

Of course we need more specific names in DOM-related contexts, e.g. tf for TextFrame, pg for Page, and so on. The table above only presents the very basic stuff, the one we deal with in the more generic functions (helpers, utilities.)

Using that minimal alphabet has an interesting side effect. When the stock of available letters begins to decline during an implementation, naming collisions and conflicts are quick to emerge. This is usually the sign that you are in fact working on a subroutine, which should be outsourced.

Function signature

I also have gradually changed the way I declare functions. Each function has a payload (relevant inputs) and purely ancillary variables. Payload items have a meaning as formal parameters while local variables just behave as inner tools, so we usually declare the latter within the function body:

const unique = function(/*any[]*/a)
//--------------------------------------
// Return a new array without duplicates.
// [ A, B, A, C, A ] ==> [ A, B, C ]
// Each elem must either be a string or
// have a toString() counterpart.
{
    var o = {},
        r = [],
        z = 0,
        n = a.length,
        i, k;
 
    for( i = 0 ; i < n ; ++i ) o['_'+a[i]] = i;
 
    for( k in o )
    {
        if( !o.hasOwnProperty(k) ) continue;
        r[z++] = a[o[k].substr(1)];
    }
 
    o = null; // Give a hint to garbage collection
    return r;
};
 

But variable declarations take up room (not to mention syntactic issues) that give them too much importance. Why do we reserve as much space to such pointless items?

In most cases we can just consider local variables as “non-formal arguments.” Thus they automatically become available, as undefined, in the body scope:

const unique = function(/*any[]*/a,  o,n,i,r,z,k)
//--------------------------------------
// Return a new array without duplicates.
// [ A, B, A, C, A ] ==> [ A, B, C ]
// Each elem must either be a string or
// have a toString() counterpart.
{
    o = {};
    for( n=a.length, i=0 ; i < n ; ++i ) o['_'+a[i]] = i;
 
    r = [];
    z = 0;
    for( k in o )
    {
        if( !o.hasOwnProperty(k) ) continue;
        r[z++] = a[o[k].substr(1)];
    }
 
    return r;
};
 

This way the signature function(/*any[]*/a, o,n,i,r,z,k) is even more significant. It shows both the expected argument(s)—I always use a conventional /*TYPE*/ prefix to highlight the payload—and at the end the unordered sequence of inner variables. o,n,i,r,z,k now sounds like a pattern that tells me much about that function. In my EDI I don't need to expand lines between the body brackets once the function is written, as the signature reveals all I'd have to check. Also, naming conflicts can be managed at a single place.

Here is a more devious example:

const clone = function F(/*any*/x,/*?ref&*/r,  k)
{
    /* . . . */
};
 

What does that signature tell us?

Since the inner name letter (F) is used, there are great chances to encounter a recursive implementation (which makes sense for a clone function.)

The payload arguments are /*any*/x and /*?ref&*/r. First is a required argument of unknown type (x), meaning that clone pretends to clone anything. Then, /*?ref&*/r refers to an optional argument (I use the prefix ? to indicate optionality) that should be a reference (in other words an Array or a generic Object) and the suffix & highlights the fact that this argument will be internally modified by the function. Hence r is an optional target, supplied by the caller, which will receive the clone of x (in case x is not a scalar.)

Finally, the function uses a key (k) and nothing else to achieve its task. This tends to indicate that both arrays and generic objects are addressed the same way (simple property looping.)

Note that there are various implementations of clone() in JS, which are more or less robust and safe. This one is just an example among others:

const clone = function F(/*any*/x,/*?ref&*/r,  k)
//--------------------------------------
{
    if( (!x) || 'object' != typeof x ) return x;
 
    r || ( r = x instanceof Array ? [] : {} );
 
    for( k in x )
    {
        x.hasOwnProperty(k) && (r[k]=F(x[k]));
    }
 
    return r;
};
 

One last example:

const gcd = function(/*int32*/x,/*int32*/y,  t)
//--------------------------------------
// Greatest Common Divisor
{
    0 > x && (x = -x);
    0 > y && (y = -y);
 
    if( y > x ){ t = x; x = y; y = t; }
 
    while( 1 )
    {
        if( !(x%=y) ) return y;
        if( !(y%=x) ) return x;
    }
};
 

Maybe a bit cryptic but pretty efficient.