11

I need to include a reference to JavaScript written by a third party on my website. Sadly, the developers that wrote this script decided to define all of their functions globally. You know, like this:

function AwesomeStringHelper() {
  // ...
}

function MyGreatFunction() {
  // ...
}

When I reference this script using a <script> tag, both of those methods will be added to the window object.

Since I prefer to not pollute the global scope, is there a way that I can change the scope of an external script? Ideally I'd like to be able to refer to these methods similar to ExternalLibrary.MyGreatFunction(), etc. I am not able to modify the third party script as it is hosted externally, and it changes frequently.

JohnD
  • 3,884
  • 1
  • 28
  • 40
  • 1
    Does it _have_ to be hosted externally? Can you proxy it via your server and modify it on the fly with code? – James Thorpe Oct 12 '16 at 15:59
  • @JamesThorpe That's an interesting suggestion, it was something I considered doing simply because the external server did not allow for client caching. – JohnD Oct 12 '16 at 16:03
  • 2
    If your only reason for wanting to do this is purely aesthetic or preferential, and it's not causing any problems, you're better off ignoring the problem. –  Oct 12 '16 at 16:21
  • @torazaburo It's not causing an issue currently, I'm just looking to see if there a better way to deal with it without a large amount of effort. You're correct though, this is completely preferential and unnecessary. – JohnD Oct 12 '16 at 16:25
  • Using [requirejs's shim](http://requirejs.org/docs/api.html#config-shim) might be quite beneficial (see [here](http://stackoverflow.com/a/14588189/5128464)). – vlp Oct 12 '16 at 22:43

5 Answers5

6

In the first instance, try to edumacate the third party developers on how to correctly write their modules.

If that doesn't work, do:

var ExternalLibrary = ExternalLibrary || window;

at the top of your code.

You can then use ExternalLibrary.MyGreatFunction() throughout to refer to their functions (even though they remain visible in the global window scope), and then later once the third party devs have fixed their scope issues then at most you need a one line change to maintain compatibility (or no change at all, if they happen to use the same ExternalLibrary name as you do).

Alternatively, use two simple snippets of code either side of the <script> tag which remember the keys of the window object, then move the newly appeared keys into a new object (at the same time deleting them from window):

Pre-load:

var ExternalLibrary = { _current: Object.keys(window) };

Post-load:

Object.keys(window).forEach(function(key) {
    if (ExternalLibrary._current.indexOf(key) < 0) {
        ExternalLibrary[key] = window[key];
        delete window[key];
    }
});
delete ExternalLibrary._current;

I've used a similar approach in the past (before strict mode was common) to check for leaking global variables.

Alnitak
  • 334,560
  • 70
  • 407
  • 495
5

If your third-party module assigns to the window object directly (like window.myGlobal = someValue), and you are able to download the source code manually, you should be able to "wrap" the entire script in a function, where the window object has been overloaded:

function wrapModule(code) {
  // create a "fake" window object that inherits from the global object
  var fakeWindow = Object.create(window);

  // create a function wrapping the code
  // note that "window" is a parameter name in this function, shadowing
  // the global object
  var func = Function("window", code);

  // call function
  func.call(fakeWindow, fakeWindow);

  // return fake window object
  return fakeWindow;
}

// run code
const fakeWindow = wrapModule(`
  var x = 0;    // local variable (will not be exported)
  y = 1;        // global variable (will still be leaked)
  window.z = 2; // assignment to window
  this.w = 3;   // assignment to this
`);

// check what variables are exposed
console.log('window.x', typeof x); // window.x undefined
console.log('window.y', typeof y); // window.y number
console.log('window.z', typeof z); // window.z undefined
console.log('window.w', typeof w); // window.w undefined

// check what variables are exposed in fakeWindow
console.log('fakeWindow.x', typeof fakeWindow.x); // fakeWindow.x undefined
console.log('fakeWindow.y', typeof fakeWindow.y); // fakeWindow.y number
console.log('fakeWindow.z', typeof fakeWindow.z); // fakeWindow.z number
console.log('fakeWindow.w', typeof fakeWindow.w); // fakeWindow.w number
Frxstrem
  • 38,761
  • 9
  • 79
  • 119
  • Yeah, sadly they do not reference window directly, it's all implicit :/ I do like your approach to this however. – JohnD Oct 12 '16 at 16:18
1

Assuming you know the specific functions being defined, then after the script is loaded, would this not work?

const ThirdPartyLib = {AwesomeStringHelper, MyGreatFunction};
delete window.AwesomeStringHelper;
delete window.MyGreatFunction;

ThirdPartyLib.AwesomeStringHelper(haveFun);
0

You can wrap the entire script in a function and return an object with the "public" functions you want, it can be tedious and hard to maintain.

var myLib = function() {
   //entire script
   return {
       functionA : functionA,
       functionB : functionB,
       //rest of functions
   }
}

Or like this (inmediately invoked function)

(function(global) {
    //entire script
    myLib.functionA = functionA;
    myLib.functionB = functionB;
    //rest of fn
    global.myLib = myLib;

})(window);

You could automate this using gulp, i'm not sure if there's a good plugin for this.

delpo
  • 210
  • 2
  • 18
  • you can get the script using gulp, not changing it and wrapping it in a function. – delpo Oct 12 '16 at 16:42
  • Sorry, @Alnitak is right. The script changes frequently and including it as part of the gulp build won't work here. It needs to be referenced to be loaded on page load. – JohnD Oct 12 '16 at 16:45
  • the clients are loading the script direct from the third party - if the OP could download, modify and serve the script directly I'm sure he would. – Alnitak Oct 12 '16 at 16:46
  • I would not recommend using external scripts because they can break your code on production, unless they use versions. Sorry about my post, i didn't get the concept of external! – delpo Oct 12 '16 at 16:50
  • I think you are on the right track, but I don't understand why this requires you to "wrap the entire script in a function", and I don't see why it would be hard to maintain. And I don't see what role there is for gulp here. Isn't this just code you would add after the ` –  Oct 12 '16 at 19:04
  • If you wrap a script in a function, the functions declared in it wont be in the window object. If you wrap it inside a function, the global scope of javascript won't have access to these functions You can generate a different script using gulp, getting the actual code, and appending an enclosing function with a structure similar/equal to the example I posted. – delpo Oct 13 '16 at 16:32
0

Not sure if jQuery is an option or if you care for it but I don't know how to write native JS AJAX calls so bear with me:

$(document).ready(function(){
    $.ajax({
        url: 'www.example.com/awesome_script.js', // get the contents of the external script
        type: 'GET',
        crossDomain: true,
        dataType: 'html',
        success: function(data){
            // build our script tag and wrap the contents inside of a function call
            var script = "<script>"
                script+= "var callMe = function(call_func, var1, var2, var3){";
                script+= data;
                script+= "return typeof call_func === 'function' ? call_func(var1, var2, var3) : 'You trying to dynamically call a variable? idk how to do that.';";
                script+= "};";
                script+= "<\/script>";

            // assuming this is legal then just append the custom script tag to the <body> :-)
            $('body').append($(script)[0]);

            // profit?
            callMe('AwesomeStringHelper', 'some_var'); // this function accepts one parameter
            callMe('MyGreatFunction'); // this function accepts no parameters
        }
    });
});
MonkeyZeus
  • 20,375
  • 4
  • 36
  • 77
  • Unfortunately CORS comes into play here and prevents me from loading the JavaScript using this method. – JohnD Oct 14 '16 at 14:03
  • @JohnD I am not familiar with all of the gotchas with CORS but it may be worthwhile to check out http://stackoverflow.com/a/20423411. Good luck though! – MonkeyZeus Oct 14 '16 at 14:26
  • @JohnD Another consideration could be to have a scheduled (hourly, nightly, weekly) task which downloads the JS file with a server-side task such as CURL in PHP and saves it to your server locally and then you can just programmatically wrap their JS code server-side similar to how I set the `var script` in my example. – MonkeyZeus Oct 14 '16 at 14:28
  • Yup, that was something James suggested in the OP's comments. – JohnD Oct 14 '16 at 14:29