5

I currently have a Web Application that runs off a global Javascript-based API, and it is initialized like this:

var Api = {
    someVar: "test",
    someFunction: function() {
        return "foo";
    }
}

This API is shared across many "Widgets" that live in the Web Application, and they should all run off this single Api instance so they can pass data to each other.

AJAX is currently used to load these Widgets, for example in widgets/mywidget.html, and it's placed in, say, <div id='widget_<random number>'>...</div>

Certain other parts of the code may choose to add more functionality to Api, and it's currently done like this:

Api.myExtension = {
    myNewFunction: function() {
        return "bar";
    }
}

However, some issues arise from this kind of usage:

Problem One: What if one Widget (these may be provided by third-parties) decides to hide some code within, and does something similar to Api = {}, destroying the global Api var everything lives on, and breaking the whole Application? Is it possible to protect this Api variable from being overwritten from outside? Only "extending" is allowed (adding new things), but "removing/changing" is not allowed. i.e.:

Api.foo = { test: "bar" } // allowed
Api.someVar = "changing the existing someVar"; // not allowed

The following code is located "inside" Api, for example:

var Api = {
    Debug: {
        Messages = new Array,
        Write: function() {
            Api.Debug.Messages.push("test"); // allowed
        }
    }
}

Api.Debug.Messages.push("test 2"); // not allowed

Probable Solutions I've Thought Of:

  1. Suppose we simply use frames to resolve this issue. The Apis provided are now separate from each other. However, there's additional overhead when loading Api again and again if I have many Widgets running, and they can no longer communicate with the "Host" of the widgets (the page where frames reside in), for example, I may want to tell the host to show a notification: Api.Notify.Show("Test"), but it cannot do so because this Api is completely independent from other instances, and it cannot communicate with the "Host"

  2. Using something like a "getter" and "setter" function for the Api to be read and written. I'm unsure on how to implement this, so any help on directions on how to implement this is welcome!

  3. A mixture of 1/2?

Jimmie Lin
  • 2,151
  • 2
  • 22
  • 34
  • I'm not a JS guru - but I don't think it can be done with JS alone. Maybe have a look into MS's new "TypeScript" it has better language features that compile down to JS, maybe that would allow you to protect it. – Jim W says reinstate Monica Oct 13 '12 at 02:32
  • 3
    Yeah, I don't *think* there's any foolproof way to avoid having a global clobbered by a third-party script (or even your own accidentally) but one thing I would suggest is using a name for your object that is less likely to be used by another script. `var TFQ = {}` is likely less prone to collision than `Api`. – ultranaut Oct 13 '12 at 02:36
  • 1
    You could try this: http://stackoverflow.com/questions/1759987/listening-for-variable-changes-in-javascript-or-jquery. Maybe it helps..;. – elclanrs Oct 13 '12 at 02:44

4 Answers4

2

There's no good way to prevent having a "third party" widget overwrite the a global variable. Generally it is the responsibility of whoever is putting together the final application to ensure that whatever JavaScripts they are using aren't littering the global namespace and conflicting. The best thing you can do in that direction is give your "Api" a nice, unique name.

What I think can help you a lot is something like the "revealing pattern", which would be a way of doing the "getters and setters" you mentioned, plus more if you needed it.

A simple, useless example would be like the following:

var Api = (function () {
    // private variable
    var myArray = [];

    return {
        addItem: function (newItem) {
            myArray.push(newItem);
        },
        printItems: function () {
            console.log("lots if items");
        }
    };
})();

Api.addItem("Hello, world");
Api.extensionValue = 5;

I think you should make a clear delineation of what is shared, or "singleton" data, and keep those items private, as with myArray in my example.

BobS
  • 2,238
  • 1
  • 13
  • 14
2

Take a look at

Object.freeze

More info here

Here is a code example from Mozilla's page:

var obj = {
  prop: function (){},
  foo: "bar"
};

// New properties may be added, existing properties may be changed or removed
obj.foo = "baz";
obj.lumpy = "woof";
delete obj.prop;

var o = Object.freeze(obj);

assert(Object.isFrozen(obj) === true);

// Now any changes will fail
obj.foo = "quux"; // silently does nothing
obj.quaxxor = "the friendly duck"; // silently doesn't add the property

// ...and in strict mode such attempts will throw TypeErrors
function fail(){
  "use strict";
  obj.foo = "sparky"; // throws a TypeError
  delete obj.quaxxor; // throws a TypeError
  obj.sparky = "arf"; // throws a TypeError
}

fail();

// Attempted changes through Object.defineProperty will also throw
Object.defineProperty(obj, "ohai", { value: 17 }); // throws a TypeError
Object.defineProperty(obj, "foo", { value: "eit" }); // throws a TypeError

However browser support is still partial

EDIT: see Kernel James's answer, it's more relevant to your question (freeze will protect the object, but not protect reassigning it. however const will) same issue with limited browser support though.

Eran Medan
  • 41,875
  • 56
  • 175
  • 268
  • Apparently this protects the object, but not protects modifying it, so this is not going to be the full solution for the problem, this with addition to @Kernel James's answer is what you are looking for probably – Eran Medan Oct 13 '12 at 05:05
2

Make it a constant:

const Api = "hi";

Api = 0;

alert(Api); //"hi"
Kernel James
  • 2,830
  • 18
  • 17
1

The only way (at least that I can think of) to protect your global variable is to prevent the Widgets from having a direct access to it. This can be achieved by using frames functions, as you suggested. You should create an object that contains all the functions that the Widgets should be able to use, and pass such to each Widget. For example:

var Api = {
   widgetApi = {
      someFunction: function(){
          // ...
      }
   },
   addWidget:function(){
      var temp = this.widgetApi.constructor(); 

      for(var key in this.widgetApi)
         temp[key] = clone(this.widgetApi[key]);
      return temp;
   }
   // Include other variables that Widgets can't use
}

This way, the Widgets could execute functions and communicate with the host or global variable Api. To set variables, the Widget would be editing its private object, rather than the global one. For every frame (that represents a Widget), you must initialize or create a copy of the widgetApi object, and probably store it inside an array, in such a way that an instance of a Widget is stored in the main Api object.

For example, given <iframe id="widget"></iframe>

You would do the following:

var widget = document.getElementById("widget");
widget.contentWindow.Api = Api.addWidget();
widget.contentWindow.parent = null;
widget.contentWindow.top = null;

Additionally, in every frame you would need to set the parent and top variables to null so that the Widgets wouldn't be able to access the data of the main frame. I haven't tested this method in a while, so there might be ways to get around setting those variables to null.

JCOC611
  • 17,315
  • 12
  • 61
  • 85
  • Thanks! However, how would the Widgets communicate with the host? How could I "watch" the Api.widgetsApi["widget"] for new "messages"? – Jimmie Lin Oct 13 '12 at 02:57
  • @JimmieLin: the functions inside the `widgetApi` can still interact with the `Api` object. So you could call other functions and edit variables from it. For example, if you had `Api.widgetApi.Debug.Messages.push()`, you could link it to the `push` function inside the global `Api`. – JCOC611 Oct 13 '12 at 03:06
  • So essentially, `Api.widgetApi.Debug.Messages.push()` within a widget is equivalent to `Api.Debug.Messages.push()` outside them? Sounds cool, thanks! – Jimmie Lin Oct 13 '12 at 03:10
  • It is, as long as you have a function inside the `widgetApi` that calls the function outside it (`this.Debug.Messages.push()`) – JCOC611 Oct 13 '12 at 03:24