Stop execution of Javascript function (client side) or tweak it

Solution 1:

Firefox currently supports the beforescriptexecute event (as of Version 4, released on March 22, 2011).

With that event and the // @run-at document-start directive, Firefox and Greasemonkey now seem to do a good job intercepting specific <script> tags.

This is still not possible for Chrome+Tampermonkey. For anything but Firefox+Greasemonkey, you will need to use techniques as shown in the other answers, below, of write a full browser extension.

The checkForBadJavascripts function encapsulates this. For example, suppose the page had a <script> tag like so:

<script>
    alert ("Sorry, Sucka!  You've got no money left.");
</script>

You could use checkForBadJavascripts like so:

checkForBadJavascripts ( [
    [   false, 
        /Sorry, Sucka/, 
        function () {
            addJS_Node ('alert ("Hooray, you\'re a millionaire.");');
        } 
    ]
] );

to get a much nicer message. (^_^)
See the inline documentation, in checkForBadJavascripts, for more.


To see a demonstration in a complete script, first visit this page at jsBin. You will see 3 lines of text, two of them added by JS.

Now, install this script (View source; it's also below.) and revisit the page. You will see that the GM-script deleted one bad tag and replaced another with our "good" JS.


Note that only Firefox supports the beforescriptexecute event. And it was removed from the HTML5 spec with no equivalent capability specified.



Complete GM script example (The same as the one at GitHub and jsBin):

Given this HTML:

<body onload="init()">
<script type="text/javascript" src="http://jsbin.com/evilExternalJS/js"></script>
<script type="text/javascript" language="javascript">
    function init () {
        var newParagraph            = document.createElement ('p');
        newParagraph.textContent    = "I was added by the old, evil init() function!";
        document.body.appendChild (newParagraph);
    }
</script>
<p>I'm some initial text.</p>
</body>


Use this Greasemonkey script:

// ==UserScript==
// @name        _Replace evil Javascript
// @include     http://jsbin.com/ogudon*
// @run-at      document-start
// ==/UserScript==

/****** New "init" function that we will use
    instead of the old, bad "init" function.
*/
function init () {
    var newParagraph            = document.createElement ('p');
    newParagraph.textContent    = "I was added by the new, good init() function!";
    document.body.appendChild (newParagraph);
}

/*--- Check for bad scripts to intercept and specify any actions to take.
*/
checkForBadJavascripts ( [
    [false, /old, evil init()/, function () {addJS_Node (init);} ],
    [true,  /evilExternalJS/i,  null ]
] );

function checkForBadJavascripts (controlArray) {
    /*--- Note that this is a self-initializing function.  The controlArray
        parameter is only active for the FIRST call.  After that, it is an
        event listener.

        The control array row is  defines like so:
        [bSearchSrcAttr, identifyingRegex, callbackFunction]
        Where:
            bSearchSrcAttr      True to search the SRC attribute of a script tag
                                false to search the TEXT content of a script tag.
            identifyingRegex    A valid regular expression that should be unique
                                to that particular script tag.
            callbackFunction    An optional function to execute when the script is
                                found.  Use null if not needed.
    */
    if ( ! controlArray.length) return null;

    checkForBadJavascripts      = function (zEvent) {

        for (var J = controlArray.length - 1;  J >= 0;  --J) {
            var bSearchSrcAttr      = controlArray[J][0];
            var identifyingRegex    = controlArray[J][1];

            if (bSearchSrcAttr) {
                if (identifyingRegex.test (zEvent.target.src) ) {
                    stopBadJavascript (J);
                    return false;
                }
            }
            else {
                if (identifyingRegex.test (zEvent.target.textContent) ) {
                    stopBadJavascript (J);
                    return false;
                }
            }
        }

        function stopBadJavascript (controlIndex) {
            zEvent.stopPropagation ();
            zEvent.preventDefault ();

            var callbackFunction    = controlArray[J][2];
            if (typeof callbackFunction == "function")
                callbackFunction ();

            //--- Remove the node just to clear clutter from Firebug inspection.
            zEvent.target.parentNode.removeChild (zEvent.target);

            //--- Script is intercepted, remove it from the list.
            controlArray.splice (J, 1);
            if ( ! controlArray.length) {
                //--- All done, remove the listener.
                window.removeEventListener (
                    'beforescriptexecute', checkForBadJavascripts, true
                );
            }
        }
    }

    /*--- Use the "beforescriptexecute" event to monitor scipts as they are loaded.
        See https://developer.mozilla.org/en/DOM/element.onbeforescriptexecute
        Note that it does not work on acripts that are dynamically created.
    */
    window.addEventListener ('beforescriptexecute', checkForBadJavascripts, true);

    return checkForBadJavascripts;
}

function addJS_Node (text, s_URL, funcToRun) {
    var D                                   = document;
    var scriptNode                          = D.createElement ('script');
    scriptNode.type                         = "text/javascript";
    if (text)       scriptNode.textContent  = text;
    if (s_URL)      scriptNode.src          = s_URL;
    if (funcToRun)  scriptNode.textContent  = '(' + funcToRun.toString() + ')()';

    var targ = D.getElementsByTagName ('head')[0] || D.body || D.documentElement;
    //--- Don't error check here. if DOM not available, should throw error.
    targ.appendChild (scriptNode);
}

Solution 2:

The answer depends on details which were not provided (The exact page and line of code would be best), but here's how you do it in general:

  1. If the offending JS code does not fire right away (Fires after DOMContentLoaded), then you can use Greasemonkey to replace the offending code.   EG:

    var scriptNode          = document.createElement ("script");
    scriptNode.textContent  = "Your JS code here";
    document.head.appendChild (scriptNode);
    

    Done.

  2. If the JS code fires immediately, then it gets more complicated.
    First, grab a copy of the script and make the desired change to it. Save this locally.

  3. Is the offending script in a file or is it in the main page HTML (<script src="Some File> versus <script>Mess O' Code</script>)?

  4. If the script is in a file, install Adblock Plus and use it to block loading of that script. Then use Greasemonkey to add your modified code to the page. EG:

    var scriptNode          = document.createElement ("script");
    scriptNode.setAttribute ("src", "Point to your modified JS file here.");
    document.head.appendChild (scriptNode);
    
  5. If the script is in the main HTML page, then install either NoScript (best) or YesScript and use it to block JavaScript from that site.
    This means that you will then need to use Greasemonkey to replace all scripts, from that site, that you do want to run.

Solution 3:

Method 1: using MutationObserver

beforescriptexecute no longer works in Firefox, nor did it work in Chrome. Luckily, there's an alternative, using MutationObserver, which is quite widely supported. The general idea is to add a MutationObserver at the beginning of pageload, which will run a callback whenever a new node is added to the DOM. Inside the callback, check for the existence of the <script> tag you want to alter or remove. If it exists, you can tamper with it (such as change its textContent, or have its src point somewhere else). Only after the callback finishes will the newly added script tag run, so this is an effective way to intercept and change Javascript on a page. Here's a live snippet example:

<script>
  // This is the userscript code, which runs at the beginning of pageload
  // Say we wanted to prevent the loading of the jQuery script tag below:
  new MutationObserver((_, observer) => {
    const jqueryScriptTag = document.querySelector('script[src*="jquery"]');
    if (jqueryScriptTag) {
      console.log('Found jQuery script tag; now removing it!');
      jqueryScriptTag.remove();
      // We've done what we needed to do, no need for the MutationObserver anymore:
      observer.disconnect();
    }
  })
    .observe(document.documentElement, { childList: true, subtree: true });
</script>
<div>Example content</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>

<script>
  console.log('After jQuery script tag. If done right, $ should be undefined:');
  console.log(typeof $);
</script>

Here's an example userscript that blocks jQuery from loading in the <head> here on Stack Overflow:

// ==UserScript==
// @name             Example jQuery removal
// @include          https://stackoverflow.com*
// @run-at           document-start
// @grant            none
// ==/UserScript==

if (document.head) {
  throw new Error('Head already exists - make sure to enable instant script injection');
}
new MutationObserver((_, observer) => {
  const jqueryScriptTag = document.querySelector('script[src*="jquery"]');
  if (jqueryScriptTag) {
    jqueryScriptTag.remove();
    observer.disconnect();
  }
})
  .observe(document.documentElement, { childList: true, subtree: true });

If you install this, you'll see that the loading of jQuery fails, resulting in lots of errors generated by Stack Overflow's JS.

Make sure to attach the MutationObserver as soon as possible - you need @run-at document-start to attach it before the page loads anything inside the <head>. (If using Tampermonkey / Chrome, you may need to enable experimental instant script injection to achieve this reliably - go to Tampermonkey Settings, Config mode: Advanced, scroll to the bottom, set Experimental Inject Mode to Instant.)

If you're writing userscripts for others, any you're using this technique, make sure to include instructions for instant script injection, since injection is not instant by default on Chrome.

Note that the observer is attached using

.observe(document.documentElement, { childList: true, subtree: true });

This attaches the observer to the <html> element, watches for added and removed immediate children with childList: true, and watches for added and removed nodes anywhere inside its descendants with subtree: true. Such a recursive listener is useful, but it's also computationally expensive on large dynamic pages, so make sure to remove it once it's achieved its purpose.

On a huge page, calling querySelector on every mutation could be costly, so you might want to iterate through the mutations (the first parameter to the observer's callback) and the mutations' addedNodes instead:

<script>
  // This is the userscript code, which runs at the beginning of pageload
  // Say we wanted to prevent the loading of the jQuery script tag below:
  new MutationObserver((mutations, observer) => {
    for (const mutation of mutations) {
      for (const addedNode of mutation.addedNodes) {
        if (addedNode.nodeType === 1 && addedNode.matches('script[src*="jquery"]')) {
          console.log('Found jQuery script tag; now removing it!');
          addedNode.remove();
          // We've done what we needed to do, no need for the MutationObserver anymore:
          observer.disconnect();
          return;
        }
      }
    }
  })
    .observe(document.documentElement, { childList: true, subtree: true });
</script>
<div>Example content</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>

<script>
  console.log('After jQuery script tag. If done right, $ should be undefined:');
  console.log(typeof $);
</script>

You can also tweak inline scripts by assigning to their textContent inside the observer callback. The following snippet shows how you can change a random number generator function to always return 10, rather than 1-6:

<script>
  // This is the userscript code, which runs at the beginning of pageload
  new MutationObserver((mutations, observer) => {
    for (const mutation of mutations) {
      for (const addedNode of mutation.addedNodes) {
        if (addedNode.textContent.includes('const randomNum')) {
          addedNode.textContent = addedNode.textContent.replace(
            /const randomNum.*/,
            'const randomNum = 10;'
          );
          observer.disconnect();
          return;
        }
      }
    }
  })
    .observe(document.documentElement, { childList: true, subtree: true });
</script>
<button>Roll Die</button>
<div>Results: <span></span></div>
<script>
const [button, span] = ['button', 'span'].map(tag => document.querySelector(tag));
button.onclick = () => {
  const randomNum = Math.ceil(Math.random() * 6);
  span.textContent = randomNum;
};
</script>

Method 2: using DevTools Local Overrides

Method 1 is the technique you can use when writing userscripts for others. But if the userscript is just for yourself, there's an easier method that can work in some situations. See this answer to the question Chrome Dev Tools - Modify javascript and reload: by going to the Sources -> Overrides tab in Chrome Devtools, and enabling local overrides, you can tell the browser to load a local copy of a .js instead of downloading the site's version. Then you can edit the local copy as desired, and it'll run instead of the built-in script. This is especially useful when tweaking large Javascript files - it's much more manageable than the MutationObserver approach.

But, there are some downsides:

  • To modify inline <script>// code here</script> tags, you'll have to download a local copy of the page. (So if the page serves dynamic content through the HTML response, you'll either have to re-save and re-tweak the .html for your overrides to use the new content, or you'll have to go back to the MutationObserver method.)
  • This is a technique developers may be able to understand (after some experimenting), but it's not so user-friendly. It may not be the sort of thing you'd want to walk a casual userscript consumer through.