Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why can't a function in a block-level scope change a formal parameter?

Tags:

javascript

;(function(a){
    if(true){
        function a(){}
    }
    console.log(a) // 1
})(1)

;(function(){
    var a = 0
    if(true){
        function a(){}
    }
    console.log(a) // function a(){}
})()

Why can't a function in a block-level scope change a formal parameter?

like image 746
Monty Yuan Avatar asked Jul 11 '18 07:07

Monty Yuan


1 Answers

I don't understand why you want to do this, but let's explore this for the sake of better understanding a corner case of JavaScript. Sometimes that helps us understand the fundamentals of the language better.

For sake of discussion, let's consider your two examples, as Case A and Case B, respectively:

// Case A - argument a is not overwritten
;(function(a){
    if(true){
        function a(){}
    }
    console.log(a) // 1
})(1)

// Case B - var a is overwritten
;(function(){
    var a = 0
    if(true){
        function a(){}
    }
    console.log(a) // function a(){}
})()

Why a function declaration in block-level scope cannot change a formal parameter

In JavaScript var does not have block scope, it has functional scope, so you cannot assume that every time you see { } that it creates a scope for the vars declared within it. Basically, function blocks work differently than other blocks as used by conditions and iterations.

Recently, with ES2015, block-scoped variables were introduced with the keywords let and const. Nevertheless, scoping is not straightforward in JS, so you must understand how different keywords create variables and how they are scoped within different block structures, and also how strict-mode affects that behavior.

As it turns out, Case B is a fluke and an accident of the way that var and function () {} declarations work in non-strict mode.

First, in all JavaScript (strict mode included), functions defined with function declarations, e.g. function foo() {...} are hoisted to the top of the current block-level scope! This means that, within scope, you can never overwrite a var by a function declaration.

// Case B modified
;(function(){
    console.log(a) // function a(){}
    var a = 0;  // overwrites value of 'a'
    function a(){}; // will be hoisted to top of block-level scope
    console.log(a) // 0
})()

Secondly, within conditional if blocks, function declarations are hoisted to the top of any block they are defined within, not the surrounding function block.

Third, in sloppy mode (non-strict), JavaScript, for function declarations defined with an if block will allow that value to overwrite the values of variables declared with var prior to that block.

// showing behavior of points #2 and #3:
;(function(){
    console.log(a); // undefined
    var a = 0;
    console.log(a); // 0
    if(true) {
       console.log(a); // function a(){...} - a() was hoisted to top of if block
       function a() {};
    })();
    console.log(a); // function a(){} - function declaration allowed to overwrite var declared above in surrounding function scope
})();

So, you've discovered a strange corner case where function declaration hoisting and scoping behave badly in non-strict mode. It won't do this in strict mode, see next section below on that.

Function arguments behave more like variables defined with let than vars, so that's why Case A doesn't behave like Case B. It's not so much that block-level scope function declarations cannot change a formal parameter as it is that it just shouldn't do that anyway, even for vars. Case A is how it ought to behave.

Note, that if you use let instead of var things behave more consistently, even in sloppy mode:

// Case B using 'let' instead
;(function(){
    let a = 0;
    console.log(a); // 0
    if(true) {
        console.log(a); // function a(){}
        function a() {};
    }
    console.log(a); 0
})();

Also, let just behaves better in general, for instance, even in sloppy mode, attempting to redefine a variable already declared with let is not allowed:

// just try this!
let a = 0;
function a() {} // this will throw a syntax error

Difference between Node and Browser? No. It's about strict mode.

Some commenters noted a difference between Node.js and JavaScript in a browser on this issue. The claim was that:

// in a browser
console.log(a) // Case A: 1
console.log(a) // Case B: function a(){}
// in node
console.log(a) // Case A: 1
console.log(a) // Case B: 0

But in fact, I just tested with both Codepen in a browser, and with Node (8.11.3 and 10.5.0) on my local, and both returned these results:

 // in Node and browser
console.log(a) // Case A: 1
console.log(a) // Case B: function a(){}

However, when you set the use strict directive, then you get the following results, but the same in both Node and browser:

 // with 'use strict, in Node and browser
console.log(a) // Case A: 1
console.log(a) // Case B: 0

Recommendations on Conditional Function Declarations

Basically, I would not do this, unless my function always returned a function. In other words, I wouldn't write a function or method, in most cases, to at times return a primitive value and other times return a function.

But let's assume you wanted to do that. Then as a matter of course, I would always:

  • use strict mode
  • use let and const instead of var See section 'Variables' here

And I would, in this case, not use a function declaration, but assign a function expression to my variable instead:

;(function(){
    let a = 0;
    console.log(a); // 0
    if(true) {
        console.log(a); // 0
        a = function () {}; // assign 'a' the value of the function
    }
    console.log(a); // function () { ... }
})();

For my own sake, I created a Codepen to help with all of this: https://codepen.io/mrchadmoore/pen/jpEKaR?editors=0012

like image 89
Chad Moore Avatar answered Nov 14 '22 22:11

Chad Moore