Tuesday, July 20, 2010

JavaScript’s evil features

Anyone who has followed Douglas Crockford’s writing or lectures knows about the “evil” parts of JavaScript: The parts that are confusing and/or that prevent us from writing clean code that performs well. The eval() function and the with statement are the two most egregious examples of evil JavaScript. Though there are other considerations, both of these features force YUI Compressor to stop replacing variables. To understand why, we need to understand the intricacies of how each works.

Working with eval()

The eval() statement’s job is to take a string and interpret it as JavaScript code. For example:

eval("alert('Hello world!');");

The tricky part of eval() is that it has access to all of the variables and functions that exist around it. Here’s a more complex example:

var message = "Hello world!";

function doSomething() {
eval("alert(message)");
}

When you call doSomething(), an alert is displayed with the message, “Hello world!”. That’s because the string passed into eval() accesses the global variable message and displays it. Now consider what would happen if you automatically replaced the variable name message:

var A = "Hello world!";

function doSomething() {
eval("alert(message)");
}

Note that changing the variable name to A results in an error when doSomething() executes (since message is undefined). YUI Compressor’s first job is to preserve the functionality of your script, and so when it sees eval(), it stops replacing variables. This might not sound like such a bad idea until you realize the full implications: Variable name replacement is prevented not only in the local context where eval() is called, but in all containing contexts as well. In the previous example, this means that both the context inside of doSomething() and the global context cannot have variable names replaced.

Using eval() anywhere in your code means that global variable names will never be changed. Consider the following example:

function handleJSONP(object) {
return object;
}

function interpretJSONP(code) {
var data = eval(code);

//process data
}

In this code, pretend that handleJSONP() and interpretJSONP() are defined in the midst of other functions. JSONP is a widely used Ajax communication format that requires the response to be interpreted by the JavaScript engine. For this example, a sample JSONP response might look like this:

handleJSONP({message:"Hello world!"});

If you received this code back from the server via an XMLHttpRequest call, the next step is to evaluate it, at which point eval() becomes very useful. But just having eval() in the code means that none of the global identifiers can have their names replaced. The best option is to limit the number of global variables you introduce.

You can often get away with this by creating a self-executing anonymous function, such as:

(function() {
function handleJSONP(object) {
return object;
}

function interpretJSONP(code) {
var data = eval(code);

//process data
}
})();

This code introduces no new global variables, but since eval() is used, none of the variable names will be replaced. The actual result (110 bytes) is:

(Line wraps marked » —Ed.)

(function(){function handleJSONP(object){return object}function »
interpretJSONP(code){var data=eval(code)}})();

The nice thing about JSONP is that it relies on the existence of just one global identifier, the function to which the result must be passed (in this case, handleJSONP()). This means that it doesn’t need access to any local variables or functions and gives you the opportunity to sequester the eval() function in its own global function. Note that you also must move handleJSONP() outside to be global as well so its name doesn’t get replaced:

//my own eval
function myEval(code) {
return eval(code);
}

function handleJSONP(object) {
return object;
}

(function() {
function interpretJSONP(code) {
var data = myEval(code);

//process data
}
})();

The function myEval() now acts like eval() except that it cannot access local variables. It can, however, access all global variables and functions. If the code being executed by eval() will never need access to local variables, then this approach is the best. By keeping the only reference to eval() outside of the anonymous function, you allow every variable name inside of that function to be replaced. Here’s the output:

function myEval(code){return eval(code)}function handleJSONP »
(a){return a}(function(){function a(b){var c=myEval(b)}})();

You can see that both interpretJSON(), code, and data were replaced (with a, b, and c, respectively). The result is 120 bytes, which you’ll note is larger than the example without eval() sequestered. That doesn’t mean the approach is faulty, it’s just that this example code is far too small to see an impact. If you were to apply this change on 100KB of JavaScript code, you would see that the resulting code is much smaller than leaving eval() in place.

Of course, the best option is not to use eval() at all, as you’ll avoid a lot of hoop-jumping to make the YUI Compressor happy. However, if you must, then sequestering the eval() function is your best bet for optimal minification.

No comments:

Post a Comment

Related Posts with Thumbnails