My JavaScript book is out! Don't miss the opportunity to upgrade your beginner or average dev skills.

Wednesday, January 30, 2013

Resurrecting The With Statement

You might think this must be a joke, well no, this is a partial lie behind the "use strict"; directive.

Roots Of The Hack

// global
"use strict";

function strict() {
  "use strict"; // top of the function

  return {
    // invoked inline
    withStrict: function(){
      return this; // undefined
    }(),

    // invoked inline too
    withoutStrict: Function("return this")()
  };

}

// the test
var really = strict();

really.withStrict;    // undefined
really.withoutStrict; // global, BOOOOM!

The Good News

I have been blaming since ever the fact that use strict makes impossible to retrieve the real global object ensuring nobody in the closure redefined window or global by accident so that code is more reliable.
Well, now we have the possibility to return it again when it's needed for security reasons or to be sure is the right one.
// a classic code for Rhino, node, and Web
var G = typeof window !== "undefined" ? window : global;
// then we need to use G

// with this hack
var global = Function("return this")();
// that's it, is the window or the global object

The Funny News: With Statement Is Back

So, we are able to deactivate the "use strict" directive in the global scope, right?
How about bringing back something that would throw an error otherwise in a strict context as with(){} is?
"use strict";
Function("with({test:123}){ alert(test) }")();
// 123
It Works!!! Awesome, we can use a with statement always be executed through Function which, differently from eval, evaluates in the global scope.

With Great Power Come Great Shenanigans

The reason number one for abandoning the with(){} statement is its ambiguity, together with the ability to pollute by mistake the global scope.
However, there were few things impossible to represent without that statement, and few of them have been proposed as the monocle mustache behavior.
array.{
    pop()
    pop()
    pop()
};

path.{
    moveTo(10, 10)
    stroke("red")
    fill("blue")
    ellipse(50, 50)
};

this.{
    foo = 17
    bar = "hello"
    baz = true
};

A Mustache Like With statement

Latter snippet is not able to pollute the global context, neither it changes context, plus it can interact with the outer scope. OK, it is not possible to implement automagically the latter one, but we can still avoid context and global context pollution ensuring a proper this value, and throwing errors if some variable does not belong to the mustached object.
How? Reactivating the "use strict"; directive again inside the non strict code: how crazy is that?
function With(o) {
  // needs a block, a function
  // can simulate that properly
  return function (f) {
    // deactivate during evaluation the strict directive
    return Function(
      // it is possible to use the with statement now
      "with(this){return(" + ("" + f).replace(
        // but we want to reactivate strict env inside
        "{", "{'use strict';"
      // avoid global context pollution
      // forcing a different this
      ) + ").call(this)}"
    ).call(o);
  };
}
So, let's see compared with previous examples, right ?
With(array)(function(){
    pop()
    pop()
    pop()
});

With(path)(function(){
    moveTo(10, 10)
    stroke("red")
    fill("blue")
    ellipse(50, 50)
});

With(this)(function(){
    foo = 17
    bar = "hello"
    baz = true
});
Does it work nested too ? Yes!
With({test:{key:"value"}})(function(){
  alert(test); // [object Object]
  With(test)(function(){
    alert(key); // "value"
  });

  // change the property
  test = 456;

  // by accident pollute the global scope
  not_defined = "oops?"; // throws an error ^_^

});
After that, removing the error at the end, the original object would shave the number 456 as test property.
In few words, we can have a secured with(){} statement behavior without the possibility to hurt the generic surrounding scope anyhow, except for those death browser without the strict directive, of course :D

Performance, Use Cases, etc

Yes, I believe the performance problem we know about that statement is still there, but with less problems to take care due strict behavior and a global environment. I would actually say that performance could be optimized with this technique, because no scope and context are implicit or modifiable anyhow, but I am not the right person to tell you what the hell happens in that case inside a JS engine :D
Use cases might be tests related, DOM related, since there things are slow in any case, or quick API prototyping due implicit return this nature of the hack: you decide :-)

Last Improvement

If you would like to adopt the technique but you want to be able to bring other local variables in that mustached block, you can use this version of the same function:
// The Strictly Monocle With Statement
function With(o,a) {
  return function(f) {
    return Function(
      "with(this){return(" + ("" + f).replace(
        "{", "{'use strict';"
      ) + ").apply(this,arguments)}"
    ).apply(o,a);
  };
}
With latest piece of code we can bring in that function whatever we need in this way:
With(
  document.body, // the implicit context
  [ // arguments to pass
    jQuery,  // jQuery
    window._ // lo-dash
  ]
)(function($, _){
  // le the magic happens
});

// or simply
With({},[1, 2])(function(a, b){
  alert([a, b]); // 1,2
});
:) Thanks for reading!

1 comment:

kentaromiura said...

http://www.reactionface.info/sites/default/files/images/YvEN9.png