hans
hans

Reputation: 99

How to overwrite Javascript functions?

I'm jusing an external JS plugin, which I initialised using this script:

    (function(g,s,t,p,l,n){
        g["_gstpln"]={};
        (l=s.createElement(t)),(n=s.getElementsByTagName(t)[0]);
        l.async=1;l.src=p;n.parentNode.insertBefore(l,n);
    })(window,document,"script","https://cdn.guestplan.com/widget.js");
   
    _gstpln.accessKey = "cebd85865b61ba61961cc749c8ca02e43cad6535";
    _gstpln.color = "#000000";
    _gstpln.showFab = false;

When listening to its event handlers, I can see it uses the function _gstpln.openWidget() & _gstpln.closeWidget() on specific buttons. I want to tweak this function a little bit, so I tried to override it in the following way:

$(document).ready(function() {
   window._gstpln.closeWidget = function() {...}

   /* also tried it without `window.` */
});

But that doesn't work, I also tried to add _gstpln.closeWidget = function() {...} at the end of the initialization script. Didn't work either.

Any ideas?

Upvotes: 0

Views: 842

Answers (1)

T.J. Crowder
T.J. Crowder

Reputation: 1074335

Since your code is run as soon as the DOM is done loading, but the plugin's code doesn't assign to closeWidget until the https://cdn.guestplan.com/widget.js file is downloaded and executed, you have a race condition going on. It's a race I think your code will very likely win, but "winning" means you assign to window._gstpln.closeWidget before the plugin does, so your assignment is overwritten by theirs.

If you an possibly achieve your purpose another way, I strongly recommend doing so. Monkey-patching a plugin like this is asking for trouble.

So how do we handle the possibility that your code gets there first? I can think of a couple of ways:

  1. By using an accessor property.
  2. By polling, checking window._gstpln.closeWidget until we see that it has a value, then overwriting the value.

The accessor property looks something like this:

$(document).ready(function() {
    let original = window._gstpln.closeWidget;
    const replacement = function() {
        // ...you can use `original` here if you need to call the
        // original implementation; presumably if `closeWidget`
        // is being called, `openWidget` has been called and so
        // the `widget.js` script has loaded...
    };
    Object.defineProperty(window._gstpln, "closeWidget", {
        get() {
            return replacement;
        },
        set(fn) {
            original = fn;
        },
    });
});

How that works:

  1. We grab the original value of closeWidget when your code runs, in case the other script has already run and filled it in.
  2. We replace it with an accessor property:
    • When the property value is read, the get function is called and returns your replacement function.
    • When the property value is written, presumably that's because your code ran before the widget.js script finished loading, and so this is that script assigning to window._gstpln.closeWidget. So that's the "original" version of the function, which we tuck away into the original variable we set up earlier.

So in theory, at that point, your code will always be run when closeWidget is run, and because there's a setter, we'll get the original function (if you need it) even if your code runs first.

Even if you don't need the original function, you probably don't want to make closeWidget read-only, because parts (at least) of widget.js are in strict mode, and assigning to read-only properties in strict mode throws an error, which would interrupt the widget.js script and probably make things not work.

Alternatively, we could make that a bit smarter by only making it an accessor if the other code didn't get there first, and redefining the property on the first set if it did:

$(document).ready(function() {
    let original = window._gstpln.closeWidget;
    const replacement = function() {
        // ...you can use `original` here if you need to call the
        // original implementation; presumably if `closeWidget`
        // is being called, `openWidget` has been called and so
        // the `widget.js` script has loaded...
    };
    if (original) {
        // The `widget.js` code got there first, the simple way should work
        // Note: This creates a read-only property, so if `widget.js` assigns
        // to it a second time, it'll (possibly) throw an error
        Object.defineProperty(window._gstpln, "closeWidget", {
            value: replacement,
        });
    } else {
        // We got there first, use an accessor that redefines on `set`
        Object.defineProperty(window._gstpln, "closeWidget", {
            get() {
                return replacement;
            },
            set(fn) {
                original = fn;
                Object.defineProperty(window._gstpln, "closeWidget", {
                    value: replacement,
                });
            },
            configurable: true, // <== So that we can change it above
        });
    }
});

The polling version looks something like this:

$(document).ready(function() {
    let timeout = Date.now() + 60000; // 60 second timeout
    check();
    function check() {
        let original = window._gstpln.closeWidget;
        if (!original) {
            if (Date.now() > timeout) {
                // Give up!
            } else {
                setTimeout(check, 50); // 50ms or whatever
            }
            return;
        }
        window._gstpln.closeWidget = function() {
            // ...you can use `original` here if you need it...
        };
    }
});

Neither of these is a good idea. But if you can't avoid it, one or the other (possibly with some tweaking) should work.

Upvotes: 1

Related Questions