ES6 Function Scoping Question

ES6 Function Scoping Question
0

#1

Could someone explain why this prints out A B B to the console, and not A B A? I thought in ES6 function declarations were now block-scopped, so defining “print” inside the if statement shouldn’t overwrite the one already on the scope, but I guess I’m not understanding something important about how scope works:

This prints A B B:

function main () {
  function print () {
    console.log('a');
  }
  print();
  if (1 > 0) {
    function print () {
      console.log('b');
    }
    print();
  }
  print();
}

main();

if I wrap in {}, it prints A B A like I’d expect:

function main () {
  {
    function print () {
      console.log('a');
    }
    print();
    if (1 > 0) {
      function print () {
        console.log('b');
      }
      print();
    }
    print();
  }
}

main();

#2

It requires strict mode to be turned on.

function function declaration get turned to function expressions when not used as source elements, so potentially also hoisting has an effect as well.


#3

Yeah, the first one makes sense to me because you are redefining print in the same scope, function level scoping. The second one, I don’t know why wrapping it in curly brackets changes everything to block level scoping, but perhaps that’s what’s happening. I don’t know ES6 well enough.


#4

change the function definitions to

let print=function() {...}

and see what happens


#5

I can see why let would work since it binds to block level scope, but I guess the other approach for declaring functions is similar to declaring:

var print = function () { }

but I’m still not sure why wrapping the entire function in {} changes the functionality?


#7

I’d avoid function statement declarations in a separate block - the spec addresses the issue here

https://www.ecma-international.org/ecma-262/8.0/index.html#sec-block-level-function-declarations-web-legacy-compatibility-semantics

stick to function expression definitions if you really want block-level function declarations


#9

@stephanpssantos - Yours prints b b b (once I add a closing } on the end of the 2nd print function) and not a b b as does the first example in the OP’s original post, so it was not interpreted the same.


#11

Ignore my previous posts, my theories were incorrect.

Function inside blocks are only scoped to that block in strict mode. Placing "use strict"; at the top of your first example will give you the behaviour you were expecting.

When not in strict mode though, as ppc has said, just avoid that pattern. In non-strict code, function declarations inside blocks behave strangely.

Reference: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions


#12

What you have encountered is probably a bug/inconsistency that is originated from function hoisting, function expression Vs function declaration.

TLDR: NEVER wrap function expressions inside blocks as you did!
(in earlier browser version caused a lot of bugs, but still now is a no no)


Premise: in JS functions are hoisted at the top of the scope, this means this is perfectly valid JS

foo(); // 'a'
function foo() {
  console.log('a');
}

Then you have block scoping. How do you expect you program to behave if the function is enclosed inside a different block? Should the function be evaluated?
Let’s look at some examples:
1: in a valid block scope

foo() // typerror: foo is not a function
if(true) {
  function foo() {
    console.log('a')
  }
}

2: however this works

if(true) {
  function foo() {
    console.log('a')
  }
}
foo() // 'a'

3: in an invalid block scope:

if(false) {
  function foo() {
    console.log('a')
  }
}
foo() // typeError foo is not a function

Now looking at your blocked example why it works? (I have no idea)

function main () {
  {
    function print () {
      console.log('a');
    }
    print();
    if (1 > 0) {
      function print () {
        console.log('b');
      }
      print();
    }
    print();
  }
}

main(); // aba

I’d expect an error of some sort, all I got is a warning from the linter.

But looks at what happens if you switch from function declaration to function expression:
(Note that function expressions are not hoisted)


function main () {
  {
    var p = function print () {
      console.log('a');
    }
    p();
    if (1 > 0) {
      var p = function print () {
        console.log('b');
      }
      p();
    }
    p();
  }
}

main(); // a b b

As you can see the result changes.
You can produce different results if you don’t re-declare var p = function print() {...}

This is one of the unfortunate inconsistencies of JS, if it were another language (maybe a compiled one) I’d expect the program to not even compile and scream at me why am I re-declaring function with the same name.

According to the implementation:

FunctionDeclarations are only allowed to appear in Program or FunctionBody. Syntactically, they can not appear in Block ({ … }) — such as that of if, while or for statements. This is because Blocks can only contain Statements, not SourceElements, which FunctionDeclaration is.
If we look at production rules carefully, we can see that the only way Expression is allowed directly within Block is when it is part of ExpressionStatement. However, ExpressionStatement is explicitly defined to not begin with “function” keyword, and this is exactly why FunctionDeclaration cannot appear directly within a Statement or Block (note that Block is merely a list of Statements).

resource:
function declaration
MDN function


#13

I’m mainly just confused because of reference: http://es6-features.org/#BlockScopedFunctions. In that example, they use the FunctionDeclaration directly in a block scope as an example. Maybe this example is just bad practice?


#14

es6 allows block-level function declarations with caveats - see the link to the section in the spec in my previous post

the fine print in the spec makes me wary of using it - even if I write a valid use case do I want to go back to the spec every time to modify the code - why do so if the same goals and clear semantics can be achieved with a let variable and a function expression?

mdn has a simple explanation but I feel it glosses over the context of this feature


#16

I’ve been reading JavaScript Allongé and thought I’d chime in some thoughts. Corrections are welcome.

I think the key is to understand how if statements work when not using block scope.

When the Javascript interpreter goes to the line where the if statement is, it evaluates to “the print function declaration that prints b to the console”.

If you run this code on your console:

if (1 > 0) {
 function print () {
  console.log('b');
 }
}
/* f print() { console.log('b'); } */

you will get the print function itself. And as expected, the print function call inside the if statement prints b to the console. A new print function is now defined.

So on the last function invocation that is print() again, its closest reference to print now is the evaluated function from the if statement.

As for the example that uses block-scope, main() returns the result of evaluating the block inside it. A block in Javascript consists of statements separated with semicolons (Javascript also knows where to put them when you forget):
{ statement_1; statement_2; statement_3; ... ; statement_n }

Scope appears to be bound to the statement itself only when block-scoped, not affecting the rest of the statements in the block unless a statement is a function declaration. Function declarations are hoisted first.

Here’s a couple of examples:

function main () {
 {
   function print () {
     console.log('a');
   }

   print();

   if (1 > 0) {
     function print () {
       console.log('b');
     }
     print();
   }

   if (true) {
     function print () {
       console.log('c');
     }
   }

   if (true) {
     function print () {
       console.log('d');
     }
   }

   print();
 }
}
main(); // a b a

And if you remove the { } inside main, which removes the block-scoping, it will print a b d (my first point).

Here’s another one still using block-scope with a function declaration somewhere in the middle.

function main () {
 {
   function print () {
     console.log('a');
   }

   print();

   if (1 > 0) {
     function print () {
       console.log('b');
     }
     print();
   }

   function print () {
       console.log('d');
   }

   if (true) {
     function print () {
       console.log('c');
     }
   }

   print();
 }
}
main(); // d b d

#17

Like I said, you need ‘use strict’; for the spec to be obeyed. Then it behaves as expected.