Search code examples
javascriptsafariscope

Mixing functions and const/let inside conditional statements cause ReferenceError in Safari


We have something like this

if(true) {
    const a = 1;
    function myFunc() {
        alert(a);
    }

    myFunc();
}

In Safari 11 this cause "ReferenceError: Can't find variable: a".

The same code works without error in Chrome and Firefox.

Using "strict mode" in Safari solve the issue.

I think that the main problem is the different scope of const a and function myFunc. The last one, in fact, is a global function due the fact that the conditional statement does not create a block scope for functions inside it (I suppose for legacy reasons) as it does for let and const.

I'm wondering if Safari has right in this case because we're mixing things with different scope.

Is there some official resource that explains this case? I don't find any mention of this behaviour either in caniuse and mdn sites 


Solution

  • Function declarations inside blocks weren't defined in the specification for many years but they were allowed by different javascript engines.

    Since this syntax was not defined in the specification and was allowed by the javascript engines, different engines did different things. Some made it a syntax error, others treated function declarations in block scopes as they were function expressions. Some engines treated functions declarations in a block scope like multiple hoisted declarations in the same scope.

    As of ES2015, function declarations are part of the specification and there are two ways they are handled:

    • Standard web semantics
    • Legacy web semantics

    Standard Semantics

    With standard semantics, function declarations are converted to function expressions, declared with let keyword and are hoisted at the top of the block. Standard semantics are in effect in strict mode.

    So in strict mode, your code will be treated by the javascript engine as though it were written like this:

    if(true) {
        let myFunc = function() {
           alert(a);
        }
    
        const a = 1;
        myFunc();
    }
    

    Legacy Web Semantics

    In non-strict mode on browsers, legacy web semantics apply. When function declarations in block scope are not treated as syntax errors, there are three scenarios that are handled the same way by all major javascript engines. Those three scenarios are:

    1. Function is declared and referenced within a single block
    2. A function is declared and possibly used within a single Block but also referenced by an inner function definition that is not contained within that same Block.
    3. A function is declared and possibly used within a single block but also referenced within subsequent blocks.

    In addition to let variable for the function defined in block scope, there's also a variable defined with var in the containing function scope or the global scope. This var assignment isn't hoisted to the top of the block and is done when the function declaration is reached in the code.

    Your code in non-strict mode is treated by javascript engine as:

    var varMyFunc;
    
    if(true) {
        let myFunc = function() {
           alert(a);
        }
    
        const a = 1;
    
        varMyFunc = myFunc;    // at the place of function declaration
       
        myFunc();
    }
    

    You shouldn't write code that relies on legacy web semantics. Instead, use strict mode to ensure that your code relies on standard rules for handling function declarations in block scopes. Having said all that, if you have legacy code in non-strict mode that relies on legacy web semantics, you can expect it to work cross-browser.