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

Tuesday, August 21, 2012

A Safer JS Environment

Oh well, apparently I wasn't joking here and I went even further ... so here I am with a weird hack you probably never thought about before ;)

A Globally Frozen Environment

Have you ever thought about this in the global context?

Object.freeze(this);

Apparently not even browser vendors such Chrome or Safari since this condition, today, is always false: Object.isFrozen(Object.freeze(this));, and even if it works as expected after freezing.
Firefox and node.js got it right while Opera Next throws an error .. but latter a part ...

Stop Any Global Pollution

That's right, if you freeze the window or global object, guess what happens here:

Object.freeze(this);

var a = 123;
alert(this.a); // undefined
alert(a); // error: a is not defined

We cannot even by mistake create a global variable ... there's no lint that could miss that.

Moreover, if you are worried about malicious code able to change some global function or constructor, you can stop worrying with proposed freeze call: that will break instantly and any manual quick test will instantly fail rather than keep going.

What About Namespaces

Well, if global namespaces, this hack will prevent the creation of any namespace.
However, we are in RequireJS and AMD module loader era where a module is imported inline through require() or inside a callback with AMD. The only important thing, is that at least the require function must be defined before this hack is performed or even that one cannot be used.

Once we have require() things are that easy, you create your own private scope and you do your own stuff in that scope being still sure that you won't pollute the global scope plus you won't be scared about other scripts ... you have your virtual sandbox

Object.freeze(this) && function (global) {
// here all local variables you want
var
fs = require("fs"),
mymodule = require("./mymodule")
;
// do the hack you want
}(this);

// AMD style
require(["fs", "./mymodule"], function (fs, mymodule) {
// already in a closure so ...
// do the hack you want
});


Too Restrictive? Object.prototype Then!

At least we can think about freezing the Object.prototype as the very first script in any webpage so the nightmare JSLint is talking about, the slow, boring, and probably already not necessary since no recent library is extending the Object.prototype since ages, hasOwnProperty() check, does not need to be in every bloody for/in 'cause you know what? Nobody can change, add, pollute, the global Object.prototype!

Object.freeze(Object.prototype);

// everything else after

for (var key in {}) {
// screw {}.hasOwnProperty, IT'S NOT NEEDED!
}



How About Both

If you are the only owner of your scripts, if you load node.js modules, by default with the ability to screw things up in the global context, or if you use AMD where global pollution should never be necessary, you might decide that this script is your best friend.

try {
// (C) WebReflection - Mit Style License
!function(global, Object){"use strict";
// buggy in both Chrome and Safari
// always false
if (!Object.isFrozen(global)) {
var freeze = Object.freeze;
Object.getOwnPropertyNames(
global
).forEach(function (prop) {
var tmp = global[prop];
switch(typeof tmp) {
case "function":
tmp = tmp.prototype;
case "object":
if (tmp) {
// console might not be freezable
// same is for String.prototype
// if applied twice and only in Safari
try {freeze(tmp)} catch(o_O) {}
}
break;
}
});
// Opera Next has some problem here
try {freeze(global)} catch(o_O) {}
}
}(this, Object);
} catch(o_O) { /* still in IE < 9 browser ... */ }

As summary, let's write down benefits of this technique:
  1. global context is free from pollution, no missed var will work anymore so it's instantly fixed before the eventual usage of a linter
  2. for/in loops could have a massive boost over object literals since nobody can possibly change the Object.prototype
  3. ES5 enabled browsers, which means all current browsers for desktop and mobile except desktop IE < 9, will prevent greedy or outdated scripts, to make the environment less secure
  4. nobody can redefine eval or Function, used sometimes for reasons but the most insecure global functions we have in JS
  5. we are forced to think in modules, 'cause there won't be any other way to load external script or dependency
  6. the only script that needs, eventually, to be loaded, will be the require() one, which means faster bootstrap by default thanks to smaller amount of synchronous bytes required to initialized our project
  7. in node.js, we are forced to write in a function even the main program, rather than polluting global object with stuff that might disturb required modules (which is true for all require/AMD based solutions too)

What do you think?

15 comments:

Eamon Nerbonne said...

The "jsperf" link with text "could have a massive boost" links to a misleading test.

- The object keys are set to be numbers, which is a unusual situation.

- The loops don't do anything except write to a function-local variable and are thus highly susceptible to dead-code elimination.

- Some of the tests mistakenly iterate over key *numbers* rather than the keys themselves, which is of course entirely uncomparable.

This version looks better: http://jsperf.com/hasownproperty-vs-for-in/17 (based largely on v.15 )

Andrea Giammarchi said...

var MyObj = new function() {
for (var i=0; i< 50;i++) this["s"+i+"e"] = MyObj2["s"+i+"e"]= i;
};

eventually ... new (function(){})() is quite big misunderstanding of how new works before functions.

Said that, thanks, I'll check the benchmark and eventually update the link

Andrea Giammarchi said...

updated ... and once again, you see you can have a massive performances boosts indeed compared with the version that uses hasOwnProperty check.

Talking about average and for all browsers ;)

Cheers

Anonymous said...

You leave out the fact that many/most 3rd party scripts would not follow this rule. Would that not cause those scripts to break too?

Andrea Giammarchi said...

which rule, the global or the Object.prototype ?

I know the global is greedy but at the same times it solves many problems we know since ever in JS world ... e.g. even undefined cannot be redefined, and the usage of noConflicts will become useless 'cause if we load modules properly and these do not pollute the global context in an obtrusive way (no matter which special char they chose as $ or _ could be) this technique does not harm.

Then there are scripts compatible with caja, this or that JS "secure" dialect while all I am suggesting here is: get it right, don't pollute the global context 'cause you don't need it if you use modules ;)

Eamon Nerbonne said...

@Andrea:
Yeah, hasOwnProperty is still a pretty hefty perf cost, I was just concerned that the original test was measuring some very unusual stuff.

Btw, what's the big misunderstanding with new (function(){})()?

Including a prototype has a certain small amount of overhead which is visible by comparing the MyObj and MyObj2 benchmarks. (I only did this for the raw access tests which isn't exactly comprehensive, sure...).

It'd be better to have most tests use the plain object rather than the one with the prototype, but oh well... time for another version :-)

Eamon Nerbonne said...

http://jsperf.com/hasownproperty-vs-for-in/18
uses new "usefully" as in actually inherits something. The perf drop is still roughtly the same.

(The motivation incidentally is that hasOwnProperty might be optimized by browsers when there's no inheritance, and new...() ensures the interpreter at least needs to look up the inheritance chain.)

Eamon Nerbonne said...

Incidentally, this object freezing is a pretty cool idea; even if it's probably not very practical while most libraries don't support it.

Andrea Giammarchi said...

Eamon, I filter comment after comment to avoid spam in this blog so just in case you don't see instantly your comment... now you know why.

Said that, if you new function(){} you are already creating a new instance while if you put brackets outside you are creating problems reading what's going on because a) not needed and b) I have to check at the end of the function body why on earth you are invoking something in that way without reasons ... drop those parenthesis and it's OK ;-)

Andrea Giammarchi said...

last but not least ... can you see the difference in mobile? Consider that when I talk about performances usually I talk about mobile where these matter.

Check the iPhone 4GS on iOS6, then others phones later on ... you see that for/in is up to 5 times faster than a loop with hasOwnProperty

Thanks for the updated test in any case, cheers

melanke said...

I think it brings more troubles than benefits

It isnt necessary at all

Unknown said...

AG - COOL

However, regarding benefit #7 of this technique - "in node.js, we are forced to write in a function even the main program, rather than polluting global object with stuff that might disturb required modules (which is true for all require/AMD based solutions too)" -

Not sure I understand this - are you saying that every Node.js module will have to be (re)written like the following?

/* whatever.js */

(function (global) {
var o = require('o');

module.exports = Whatever;

Whatever = function () {};

// or Whatever = {}

// or return { id: 'Whatever' };

}(this));

Regards,
DKaye

Unknown said...

AG - here's another one - doesn't this prevent us from using JSON-P callbacks, as on google-plus, for example?

Andrea Giammarchi said...

only the main program shoul deventually be written like that ... modules do not need it since these are exposing as context the module itself, not the global object ... so

if (this.parent) {
// it's a module
} else {
// it's global, can't use variables :-)
}

JSONP callbacks should work through a JSONP object rather than polluting the global scope for no reason/benefit so yes, I guess JSONP callbacks are forced to be less obtrusive, which would be ideal.

Mark said...

Very good article!
It is strange that this code is almost never used ...
Why??