Skip to content

Instantly share code, notes, and snippets.

@BolajiAyodeji
Forked from getify/1.md
Created March 13, 2019 21:28
Show Gist options
  • Save BolajiAyodeji/cffed1052f4dd72f910dae768f8a6b76 to your computer and use it in GitHub Desktop.
Save BolajiAyodeji/cffed1052f4dd72f910dae768f8a6b76 to your computer and use it in GitHub Desktop.
A question about JS parameter scopes, and closures over them vs function scopes

I received a question about this snippet of code:

function def(first="oldValue" , second=function(){
         return first;
}){
        var first="updatedValue";
        console.log('inside',first);
        console.log('function',second());
}

When you run that function without any arguments (like def();), you get this:

inside updatedValue
function oldValue

But if you change the code to this:

function def(first="oldValue" , second=function(){
         return first;
}){
        first="updatedValue";           // NOTE: removed the `var` here
        console.log('inside',first);
        console.log('function',second());
}

Now it prints:

inside updatedValue
function updatedValue

The question is, why is this happening?

NOTE: I strongly advise against venturing into these murky/confusing waters, where you create closures inside a parameter default value, and especially if you're going to then shadow those parameters as local function variables. This is way into the weeds and while it may be interesting for JS trivia, you should never rely on this kind of behavior in your programs. It's far too confusing to the vast majority of readers.


There's two layers of answer here. So, first:

We normally think of parameters and local variables as being the same -- coming from the same local function scope. But technically, they've always been in separate scopes. It's just that this is the first time (ES6+) we've been able to observe those different scopes.

When there's a var first = .. in the function, it "shadows" the parameter variable (which is technically in its own scope), so that local variable is created separately from the parameter. That's why you get the two separate values. The default function value closes over the first in the parameter scope, but the first in the main function scope has a different value.

When you take the var off, the first = .. is just a plain assignment to a lexical variable, and it finds the one in the parameter scope to overwrite its value. That's why you get both printed values being the same in that case.

Wait. Re-read this (repeatedly if necessary) until you can convince yourself of this reasoning and be comfortable with it.

If that's good enough (or confusing enough!) for you, just take the blue pill and stop. Refer to my advice about "JS trivia" above and move on.


OK, so you want to take the red pill? Here we go.

Consider this function:

function def(first="oldValue" , second=function(){
         return first;
}){
        var first;                    // NOTE: no assignment, just declaration
        console.log('inside',first);
        console.log('function',second());
}

If you were using the logic we've just articulated, what would you expect that function to print out?

I think the most reasonable expectation would be:

inside undefined
function oldValue

Why? Because you've created the first in the local scope, but it should just be in an undefined state since it's not been assigned anything.

BUT! That's not what happens. It prints:

inside oldValue
function oldValue

Why?

Because of legacy expected behavior from before ES6, this special case means that the shadowed local variable declaration in the function is by default set to the value of the parameter, NOT overwritten by the implied = undefined from var first; as one would normally expect.

However, they are still two separate variables; they just happen to have the same values at the moment. As soon as you assign, you're changing only the local version of that variable:

function def(first="oldValue" , second=function(){
         return first;
}){
        var first;

        // later
        first = "updatedValue";

        console.log('inside',first);
        console.log('function',second());
}

It will now print:

inside updatedValue
function oldValue

Yay for confusing scope behaviors and legacy corner case exceptions!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment