Remedial JavaScript

Douglas Crockford
www.crockford.com

The JavaScript Programming Language suffers from premature standardization. It was rushed to market and then instantly had a huge user base and, soon after, a formal international standard. There was no time during its development to polish the language. As a result, it still has some rough spots.

Some of the problems we can easily avoid by not using misfeatures like the with statement. A lint program can be helpful in keeping us clear of some of the sharp edges.

Some feature omissions can be corrected by adding new functions and basic methods to our standard programming toolkit. That is what we will be doing here. There are functions that I feel should have been in the Standard and required in every implementation. Fortunately, JavaScript is such an expressive language that we can easily fix the omissions locally.

JavaScript is most used in web applications, and there is still an incompatibility legacy left over from the Browser Wars. For a time, Netscape and Microsoft were frantically adding features to their browsers, sometimes imitating each other, sometimes planting proprietary hooks, sometimes incorrectly anticipating a future ECMAScript Language Specification.

Type Detection

Since JavaScript is a loosely-typed language, it is sometimes necessary to examine a value to determine its type. (This is sometimes necessary in strongly typed languages as well.) JavaScript provides a typeof operator to facilitate this, but typeof has problems.

Object 'object'
Array 'object'
Function 'function'
String 'string'
Number 'number'
Boolean 'boolean'
null 'object'
undefined 'undefined'

typeof [] produces 'object' instead of 'array'. That isn't totally wrong since arrays in JavaScript inherit from objects, but it isn't very useful. typeof null produces 'object' instead of 'null'. That is totally wrong.

We can correct this by defining our own typeOf function, which we can use in place of the defective typeof operator.

isEmpty(v)

isEmpty(v) returns true if v is an object containing no enumerable members.

String Methods

JavaScript provides some useful methods for strings, but leaves out some important ones. Fortunately, JavaScript allows us to add new methods to the basic types.

entityify()

entityify() produces a string in which '<', '>', and '&' are replaced with their HTML entity equivalents. This is essential for placing arbitrary strings into HTML texts. So,

"if (a < b && b > c) {".entityify()

produces

"if (a &lt; b &amp;&amp; b &gt; c) {"

quote()

quote() produces a quoted string. This method returns a string that is like the original string except that it is wrapped in quotes and all quote and backslash characters are preceded with backslash.

supplant(object)

supplant() does variable substitution on the string. It scans through the string looking for expressions enclosed in { } braces. If an expression is found, use it as a key on the object, and if the key has a string value or number value, it is substituted for the bracket expression and it repeats. This is useful for automatically fixing URLs. So

param = {domain: 'valvion.com', media: 'http://media.valvion.com/'};
url = "{media}logo.gif".supplant(param);

produces a url containing "http://media.valvion.com/logo.gif".

trim()

The trim() method removes whitespace characters from the beginning and end of the string.

Deployment

You can put these functions in your code library and copy them individually into your projects as you need them. Or you can put them all in a single file that you include in all of your projects, so you can always count on having a familiar platform to work from. Be sure to process the file with JSMin in order to reduce download time. You may have useful toolkit functions of your own to include.

Source

In the source code that follows, the priorities were portability and compactness.

function typeOf(value) {
var s = typeof value;
if (s === 'object') {
if (value) {
if (value instanceof Array) {
s = 'array';
}
} else {
s = 'null';
}
}
return s;
}

The typeOf function above will only recognize arrays that are created in the same context (or window or frame). If we want to recognize arrays that are constructed in a different frame, then we need to do something more complicated.

function typeOf(value) {
    var s = typeof value;
    if (s === 'object') {
        if (value) {
            if (Object.prototype.toString.call(value) == '[object Array]') {
                s = 'array';
            }
        } else {
            s = 'null';
        }
    }
    return s;
}


function isEmpty(o) {
    var i, v;
    if (typeOf(o) === 'object') {
        for (i in o) {
            v = o[i];
            if (v !== undefined && typeOf(v) !== 'function') {
                return false;
            }
        }
    }
    return true;
}

if (!String.prototype.entityify) {
    String.prototype.entityify = function () {
        return this.replace(/&/g, "&amp;").replace(/</g,
"&lt;").replace(/>/g, "&gt;"); }; } if (!String.prototype.quote) { String.prototype.quote = function () { var c, i, l = this.length, o = '"'; for (i = 0; i < l; i += 1) { c = this.charAt(i); if (c >= ' ') { if (c === '\\' || c === '"') { o += '\\'; } o += c; } else { switch (c) { case '\b': o += '\\b'; break; case '\f': o += '\\f'; break; case '\n': o += '\\n'; break; case '\r': o += '\\r'; break; case '\t': o += '\\t'; break; default: c = c.charCodeAt(); o += '\\u00' + Math.floor(c / 16).toString(16) + (c % 16).toString(16); } } } return o + '"'; }; } if (!String.prototype.supplant) { String.prototype.supplant = function (o) { return this.replace( /\{([^{}]*)\}/g, function (a, b) { var r = o[b]; return typeof r === 'string' || typeof r === 'number' ? r : a; } ); }; } if (!String.prototype.trim) { String.prototype.trim = function () { return this.replace(/^\s*(\S*(?:\s+\S+)*)\s*$/, "$1"); }; }