In order to enable Kitt, our iPhone web browser, to run browser extensions, we needed a way to run content scripts in a webpage. In Chrome, content scripts are run in a sandbox to prevent the two contexts from interfering with each other. They share only the window
object. Although there are tantalizing signs that something similar might be possible on iOS 7, we weren’t sure whether this would be acceptable to Apple, so we decided to inject content scripts straight into the webpage and isolate them using closures.
This has been surprisingly successful, but we did start to run into problems when a content script uses jQuery on a webpage that also uses jQuery. The problem is the shared DOM. jQuery (and many other JavaScript frameworks) create a global symbol by hanging a new property off the window object. Since the window is shared, the window.$
might get overwritten. This doesn’t happen in Chrome because the DOM window isn’t really shared with the content scripts. Instead, a wrapper is used to isolate the JavaScript contexts so they only see their own expando properties. In this way each context can have a different window.$
property.
At first glance, writing a complete wrapper that explicitly wraps every window property seemed like a big effort, so we first tried to solve the immediate problem of jQuery conflicts in the easiest manner possible (some might call this laziness but I prefer to think of it as “proof of concept”):
var wrapper = Object.create(window);
(function(window) {
// content script
})(wrapper);
By using Object.create
we get a wrapper whose prototype is the original window. The content scripts can still see the window’s expando properties, unless it overrides them, but the reverse is not true. Mission accomplished: loading jQuery into a content script will not mess up the native jQuery of the webpage. Just one problem: when we took this super lightweight wrapper for a test drive, we got an “Illegal invocation” error every time we tried to call one of its methods. Turns out that when you call methods on a DOM window, the this
pointer has to point at the actual window object. Attempt number two looked like this:
var wrapper = Object.create(window);
for (var prop in window) {
if (typeof(window[prop]) === 'function') {
(function(prop) {
wrapper[prop] = function() { return window[prop].apply(window, arguments); }
})(prop);
}
}
This solves the “illegal invocation” problem. Right away, however, another issue cropped up. Content scripts that use JavaScript are likely to do so by accessing the $
symbol on the global object rather than referencing it as window.$
. So they will still be using the jQuery from the webpage. We could override the $
symbol explicitly, but this would be a jQuery-only solution that wouldn’t work for other libraries. After some head-scratching, we realized that we could create our own pseudo-global object using with
:
(function(window) {
with (window) {
// content script
}
})(wrapper);
As my colleague pointed out, usage of with
is generally frowned on due to the very characteristic we are exploiting here. Unfortunately, jQuery was still malfunctioning. More investigation revealed that the cause was the jQuery.isWindow
function. That’s right, jQuery doesn’t consider an object to be a window unless it is equal to its own window
property. And while we never found any explanation for this, our wrapper resisted all efforts to change the value of this property when using the DOM window as its prototype.
At this point, we gave up on the prototype altogther and wrapped everything:
var wrapper = {};
for (var prop in window) {
(function(prop) {
if (typeof(window[prop]) === 'function') {
wrapper[prop] = function() { return window[prop].apply(window, arguments); }
}
else {
Object.defineProperty(wrapper, prop, {
'get': function() {
if (window[prop] === window) {
return wrapper;
}
else {
return window[prop];
}
},
'set': function(value) { wrapper[prop] = value; }
});
})(prop);
}
That’s the wrapper we’re using now and, combined with usage of with
as described above, it seems to be a pretty good approximation of the wrapper used in Chrome content scripts. And while it’s considerably longer than our initial naive attempt, it isn’t as complex as we had feared. There’s a lesson in there somewhere.
Update: Implementors should also read our follow-up post.