18

General question: When a default Javascript prototype like Array has been modified, hacked, changed and twisted to the point of being unusable, is there any way to create instances of (or re-implement) the original, un-modified prototype?


My case: I've got some code that is failing in the 'edit' mode of a (horrible, proprietary, closed source...) content management system, because the javascript for the interface of the 'edit' mode of the content management system hacks the absolute living hell out of the Array prototype.

My code will work in the non-edit mode of the CMS, but, to get there, it has be tested in the 'edit' mode. It's possible to test if a prototype has been modified. Is it possible to re-implement the default Array prototype so I could do something like this:

var hasArrayBeenTrashed = // boolean based on https://stackoverflow.com/questions/574584/
var normalArray.prototype = // based on answer to this question 
var myArray = !hasArrayBeenTrashed ? [] : new normalArray;
Community
  • 1
  • 1
user56reinstatemonica8
  • 32,576
  • 21
  • 101
  • 125
  • 3
    You can always create an empty `iframe` to create a new JavaScript context and get references to unmodified structures from there. But some things, like `arr instanceof Array`, will fail then. I think something similar has been asked before, will try to find it. – Felix Kling Dec 21 '12 at 12:30
  • Probbaly not much use in the environment you need, but you could probably utilise a WebWorker to create a new context, perform operations in the safe environment then post the results back. – Graham Dec 21 '12 at 12:34
  • @FelixKling Ingenious idea! I'll try it. I do need `arr instanceof Array`, but maybe if I just clone Array as normalArray even when it's not modified I can still use `arr instanceof normalArray` ? I did look for dupes first so if there are any, they probably use different search terms. – user56reinstatemonica8 Dec 21 '12 at 12:40

2 Answers2

24

OP's probably already figured something out by now, but for anyone else coming in from a Google search or wherever, here's a function that returns the unmodified version of any default constructor passed to it:

// Note: the double name assignment below is intentional.
// Only change this part if you want to use a different variable name.
//  │││││ The other one here needs to stay the same for internal reference.
//  ↓↓↓↓↓            ↓↓↓↓↓
var reset = function reset(constructor) {
    if (!(constructor.name in reset)) {
        var iframe = document.createElement('iframe');
        iframe.src = 'about:blank';
        document.body.appendChild(iframe);
        reset[constructor.name] = iframe.contentWindow[constructor.name];
        document.body.removeChild(iframe);
    } return reset[constructor.name];
}

Usage goes like this:

The Problem

Somebody does something stupid to a default prototype...

Array.prototype.push = function () {
    var that = this;
    [].forEach.call(arguments, function (argument) {
        that.splice(Math.round(Math.random()*that.length), 0, argument)
    }); return 'Trolololo';
}

...and your code becomes a broken mess.

var myArray = new Array(0, 1, 2, 3);
//-> undefined
    // Ok, I made an array.
myArray;
//-> [0, 1, 2, 3]
    // So far so good...
myArray.push(4, 5);
//-> "Trolololo"
    // What?
myArray;
//-> [5, 0, 1, 2, 4, 3]
    // WHAT!?

The Solution

So you throw that function into the mix...

var reset = function reset(constructor) {
    if (!(constructor.name in reset)) {
        var iframe = document.createElement('iframe');
        iframe.src = 'about:blank';
        document.body.appendChild(iframe);
        reset[constructor.name] = iframe.contentWindow[constructor.name];
        document.body.removeChild(iframe);
    } return reset[constructor.name];
}

...and put it to use like so.

var myArray = new reset(Array)(0, 1, 2, 3);
//-> undefined
    // Looks the same
myArray;
//-> [0, 1, 2, 3]
    // Still looks the same
myArray.push(4, 5);
//-> 6
    // Hey, it returned what it's supposed to...
myArray;
//-> [0, 1, 2, 3, 4, 5]
    // ...and all's right with the world again!

Also, because each reset constructor is cached the first time it's returned, you can save a character if you want to by referencing the cache directly (reset.Array) instead of through the function (reset(Array)) every time after that.


Good luck!

Marl
  • 412
  • 4
  • 4
  • 1
    This reset function throws error Function expected in IE 10,11 – NeiL Nov 03 '16 at 12:03
  • I am trying this on a page that is overwriting the `Promise` constructor, and I tried running `reset(Promise)` in Chrome, and it is telling me `Uncaught TypeError: Promise is not a constructor`. – Elias Zamaria Dec 23 '21 at 19:17
  • I have never thought to use a function definition's own name as a house for static variables. This is very interesting, and I'm already thinking of many patterns to use with this. Thank you for teaching me something new from so long ago! – jsea Jun 21 '22 at 06:29
6

You could just copy Array methods from the iframe:

Array.prototype.slice = function() {
    return "trololol";
};
var a = document.createElement("iframe");
a.src = "about:blank";
document.body.appendChild(a);
var prototype = a.contentWindow.Array.prototype;
var fn = ["toString", "toLocaleString", "join", "pop", "push", "concat", "reverse", "shift", "unshift", "slice", "splice", "sort", "filter", "forEach", "some", "every", "map", "indexOf", "lastIndexOf", "reduce", "reduceRight"];
for (var i = 0; i < fn.length; ++i) {
    var methodName = fn[i];
    var method = prototype[methodName];
    if (method) {
        Array.prototype[methodName] = method;
    }
}
document.body.removeChild(a);

​ Here's the jsfiddle that works in chrome and IE9, don't have time to figure out IE7-8. http://jsfiddle.net/jMUur/1/

To check if an object is an Array without relying on references:

function isArray( obj ) {
     return {}.toString.call( obj ) === "[object Array]";
}
Esailija
  • 138,174
  • 23
  • 272
  • 326
  • Looks great - but how would I modify this to create a new type of object based on array, rather than overwriting Array? I imagine undoing the CMS's Array prototype hacking will break the CMS - say I wanted to use a new class based on Array, `normalArray`, instead of Array in the line `Array.prototype[methodName] = method;`? Thanks – user56reinstatemonica8 Dec 21 '12 at 13:04
  • I realise that's a bit different to the title of the question, editting to better reflect intent. – user56reinstatemonica8 Dec 21 '12 at 13:05
  • 1
    @user568458 that doesn't really work well, in some browsers the iframed array will still return array instances of main window, like the method .slice – Esailija Dec 21 '12 at 13:06