2

What's the best way to conditionally define functions for popular modern browsers?

I'm seeing Safari 12 (iOS 12.0.1 and macOS 10.14) incorrectly define conditional functions while Chrome, Firefox, Edge seem to do the right thing. Additionally, what's the spec correct behavior for a JS engine so I could report this bug to Apple if it's a bug.

Background: We're doing this to avoid Google Analytics (gtag.js) from ever loading if a user doesn't consent to GDPR. Google's gtags.js boilerplate init code has a line function gtag(){dataLayer.push(arguments);} which trips Safari.

Code below, demo at JS Fiddle here

<html>
  <head>
    <title>Safari JS engine test</title>
    <script type="text/javascript">
      if (false) {
        function conditionalFunctionality() {
          alert("Doing conditional functionality");
        }
      }
      window.onload = function() {
        if (typeof conditionalFunctionality === "function") {
          alert("BAD. It's defined");
        } else {
          alert("GOOD. It's NOT defined");
        }
      }
    </script>
  </head>
  <body>
    <h1>Safari JS engine test</h1>
  </body>
</html>

Mugshots

iOS macOS

DeepSpace101
  • 13,110
  • 9
  • 77
  • 127
  • 2
    Don't put function declarations inside conditional blocks. It doesn't make good sense and it's disallowed in "strict" mode anyway. – Pointy Oct 19 '18 at 17:53
  • I think you might want to try doing `conditionalFunctionality = function() { … }`. function definitions are subject to some funky rules if I remember right – Sam Mason Oct 19 '18 at 17:57
  • @Pointy: In our case, functions are being defined at root level based on conditional functionality (e.g. GDPR opt-in). child events/pages call based on the `typeof conditionalFunctionality === "function"`. What's the suggested path? I don't see this disallowed in strict mode, link? – DeepSpace101 Oct 19 '18 at 18:10
  • 2
    The problem is that by the spec, a `function` declaration statement (as in your example posted, and as opposed to a function instantiation *expression*) is hoisted to the top of the function scope. Thus the meaning of a function with a function declaration inside a conditional block is ambiguous; to solve that, modern (strict) JavaScript simply disallows it. You can use plain variables and function expressions to achieve the same basic result without the ambiguity. – Pointy Oct 19 '18 at 18:12
  • @SamMason: Your code works because of the reasons Pointy mentioned (details: https://gomakethings.com/function-expressions-vs-function-declarations/). If you can put it in an answer, I'd like to mark it as the answer – DeepSpace101 Oct 19 '18 at 21:45

2 Answers2

1

As mentioned by Pointy's comment, you don't want to conditionally define functions. Instead, you want to define the function for all browsers, and then execute code in the body conditionally depending on the browser.

This stackoverflow post goes into detail of how to detect certain browsers, so I won't repeat that here: How to detect Safari, Chrome, IE, Firefox and Opera browser?

you can then write something like

function f(...) {
    if (isSafari()) {
        // Safari specific stuff
    else {
        // other stuff
    }
}

where the isSafari function is a wrapper around a check from the stackoverflow post I linked.

EDIT: it's been pointed out that you want this based on specific functionality, rather than browsers. In that case, you should replace isSafari in my example above with an appropriate function to check that the current browser supports the desired functionality, rather than checking the browser type.

The link I provided actually checks the functionality of the browser to determine which browser is being used, so you should be able to use that to solve your issue.

CoffeeTableEspresso
  • 2,614
  • 1
  • 12
  • 30
  • In our case, functions are being defined based on functionality (e.g. GDPR opt-in) rather than browsers. – DeepSpace101 Oct 19 '18 at 18:07
  • @DeepSpace101 I've edited it. The accepted answer on the link I provided shows how to check the functionality of the browser, as this is the safest way of determining which browser is in use. This should apply to your specific use case. – CoffeeTableEspresso Oct 19 '18 at 18:38
1

As indicated in the comments above, function definitions can behave non-intuitively when they aren't "top level". For example:

a()
function a() { console.log("here"); }

will log to the console even though the function definition appears after it's used. This is termed hoisting in Javascript, and a good description is available here:

https://gomakethings.com/function-expressions-vs-function-declarations/

(courtesy of pointy)

Different browser versions perform this hoisting differently, leading to the inconsistency you're observing and why "strict" mode (i.e. having "use strict"; at the top of the code block alters this behaviour).

When I want to get behaviour like this I tend to use "function expressions". Which would look more like this:

var conditionalFunctionality;
if (false) {
  conditionalFunctionality = function () {
    console.log("conditionally defined");
  }
}
if (conditionalFunctionality) {
  conditionalFunctionality();
}

which also has the advantage of having a naturally "falsy" variable as appropriate and not needing to play any typeof conditionalFunctionality === "function" games.

Sam Mason
  • 15,216
  • 1
  • 41
  • 60