Are there legitimate uses for JavaScript's "with" statement?
Alan Storm's comments in response to my answer regarding the with
statement got me thinking. I've seldom found a reason to use this particular language feature, and had never given much thought to how it might cause trouble. Now, I'm curious as to how I might make effective use of with
, while avoiding its pitfalls.
Where have you found the with
statement useful?
Another use occurred to me today, so I searched the web excitedly and found an existing mention of it: Defining Variables inside Block Scope.
Background
JavaScript, in spite of its superficial resemblance to C and C++, does not scope variables to the block they are defined in:
var name = "Joe";
if ( true )
{
var name = "Jack";
}
// name now contains "Jack"
Declaring a closure in a loop is a common task where this can lead to errors:
for (var i=0; i<3; ++i)
{
var num = i;
setTimeout(function() { alert(num); }, 10);
}
Because the for loop does not introduce a new scope, the same num
- with a value of 2
- will be shared by all three functions.
A new scope: let
and with
With the introduction of the let
statement in ES6, it becomes easy to introduce a new scope when necessary to avoid these problems:
// variables introduced in this statement
// are scoped to each iteration of the loop
for (let i=0; i<3; ++i)
{
setTimeout(function() { alert(i); }, 10);
}
Or even:
for (var i=0; i<3; ++i)
{
// variables introduced in this statement
// are scoped to the block containing it.
let num = i;
setTimeout(function() { alert(num); }, 10);
}
Until ES6 is universally available, this use remains limited to the newest browsers and developers willing to use transpilers. However, we can easily simulate this behavior using with
:
for (var i=0; i<3; ++i)
{
// object members introduced in this statement
// are scoped to the block following it.
with ({num: i})
{
setTimeout(function() { alert(num); }, 10);
}
}
The loop now works as intended, creating three separate variables with values from 0 to 2. Note that variables declared within the block are not scoped to it, unlike the behavior of blocks in C++ (in C, variables must be declared at the start of a block, so in a way it is similar). This behavior is actually quite similar to a let
block syntax introduced in earlier versions of Mozilla browsers, but not widely adopted elsewhere.
I have been using the with statement as a simple form of scoped import. Let's say you have a markup builder of some sort. Rather than writing:
markupbuilder.div(
markupbuilder.p('Hi! I am a paragraph!',
markupbuilder.span('I am a span inside a paragraph')
)
)
You could instead write:
with(markupbuilder){
div(
p('Hi! I am a paragraph!',
span('I am a span inside a paragraph')
)
)
}
For this use case, I am not doing any assignment, so I don't have the ambiguity problem associated with that.
As my previous comments indicated, I don't think you can use with
safely no matter how tempting it might be in any given situation. Since the issue isn't directly covered here, I'll repeat it. Consider the following code
user = {};
someFunctionThatDoesStuffToUser(user);
someOtherFunction(user);
with(user){
name = 'Bob';
age = 20;
}
Without carefully investigating those function calls, there's no way to tell what the state of your program will be after this code runs. If user.name
was already set, it will now be Bob
. If it wasn't set, the global name
will be initialized or changed to Bob
and the user
object will remain without a name
property.
Bugs happen. If you use with you will eventually do this and increase the chances your program will fail. Worse, you may encounter working code that sets a global in the with block, either deliberately or through the author not knowing about this quirk of the construct. It's a lot like encountering fall through on a switch, you have no idea if the author intended this and there's no way to know if "fixing" the code will introduce a regression.
Modern programming languages are chocked full of features. Some features, after years of use, are discovered to be bad, and should be avoided. Javascript's with
is one of them.
I actually found the with
statement to be incredibly useful recently. This technique never really occurred to me until I started my current project - a command line console written in JavaScript. I was trying to emulate the Firebug/WebKit console APIs where special commands can be entered into the console but they don't override any variables in the global scope. I thought of this when trying to overcome a problem I mentioned in the comments to Shog9's excellent answer.
To achieve this effect, I used two with statements to "layer" a scope behind the global scope:
with (consoleCommands) {
with (window) {
eval(expression);
}
}
The great thing about this technique is that, aside from the performance disadvantages, it doesn't suffer the usual fears of the with
statement, because we're evaluating in the global scope anyway - there's no danger of variables outside our pseudo-scope from being modified.
I was inspired to post this answer when, to my surprise, I managed to find the same technique used elsewhere - the Chromium source code!
InjectedScript._evaluateOn = function(evalFunction, object, expression) {
InjectedScript._ensureCommandLineAPIInstalled();
// Surround the expression in with statements to inject our command line API so that
// the window object properties still take more precedent than our API functions.
expression = "with (window._inspectorCommandLineAPI) { with (window) { " + expression + " } }";
return evalFunction.call(object, expression);
}
EDIT: Just checked the Firebug source, they chain 4 with statements together for even more layers. Crazy!
const evalScript = "with (__win__.__scope__.vars) { with (__win__.__scope__.api) { with (__win__.__scope__.userVars) { with (__win__) {" +
"try {" +
"__win__.__scope__.callback(eval(__win__.__scope__.expr));" +
"} catch (exc) {" +
"__win__.__scope__.callback(exc, true);" +
"}" +
"}}}}";