0

(edit) I found out there's another post: "Where are arguments positioned in the lexical environment?" that is directly related to my question.


When I was reading "You Don't Konw JS", I did some experiments to shadow parameters by declaring local variables in the function, and then I met some weird situations that I am not able to explain, so I posted my code here to seek some help.

Please take a look at the "case 2" of the following code, and see if you can tell what's happening, thanks.

My questions: (in "case 2")

  • Are "parameter list" and "function body" in the same scope ?
    • if so, var id = 5 should be considered a "redeclaration", parameter id would be updated by the redeclaration, but it's NOT, why ? (see: (***) below)
    • if they're not in the same scope, why can't we shadow the (outer) parameter id with a local "let", like we do in a "for-loop" ? (see: (*) below)

const { log } = console;

let count = 0;

// (*)
// ⭐ "initialization block" in a for-loop
// ---------------------------------------
//   ╭──init───╮
for ( let i = 0 ; i < 3 ; i++ ) {

    // ⭐ for-loop body (block scope) and the "initialization block"
    //    are in different scopes, so we can "shadow" the outer `i`
    //    with a local "let" without any problem.
    let i = 1;
    
    log(i);                    // 1, 1, 1
    if (++count > 3) break;    // prevent infinite loop
}

// ⭐ case 1: (alters parameter directly)
// ---------------------------------------
// • parameter `id` is closed over by `defaultID`.
//
//                ╭─── parameter list ───╮
function doUpdate(id, defaultID = () => id) {
    id = 5;                  // ✅ parameter `id` updated (see (**) below)
    log( defaultID() );
}

// (**)
doUpdate(3);                 // ✅ 5

// ⭐ case 2: (shadows parameter by local "var")
// ----------------------------------------------
//
//                    ╭─── parameter list ───╮
function doesntUpdate(id, defaultID = () => id) {

    // ----------------------
    //  ❓ weird situation ❓
    // ----------------------
    
    // ❗ 2.1: can't shadow parameters by "let" variables
    // ----------------------------------------------------------
    // let id = 5;
    //     ^^
    // ⛔ SyntaxError: Identifier 'id' has already been declared
    // ----------------------------------------------------------

    log( defaultID() );      // 3

    // ⭐ 2.2 use "var" instead:
    // -------------------------
    
    var id = 5;              // ❗ this do shadow parameter `id`
    log(id);                 // 5

    log( defaultID() );      // 3
    
    // ----------------------------------------------------------------
    // ⭐ are "parameter list" and "function body" in the same scope ❓
    // ----------------------------------------------------------------
    //
    //   • if so, `var id = 5` should be considered a "redeclaration",
    //     parameter `id` would be updated by the redeclaration, 
    //     but it's NOT, why ❓   (see: (***) below)
    //     (Q: is parameter `i` a "var" ?) 
    // 
    //   • if they're not in the same scope, why can't we shadow 
    //     the (outer) parameter `id` with a local "let", like we
    //     do in a "for-loop"❓  (see: (*) above)
    //
    // ----------------------------------------------------------------

}

// (***)
doesntUpdate(3);             // ❗ (parameter `id` is not updated)
lochiwei
  • 1,240
  • 9
  • 16
  • 1
    in the "case 2", the local variable `id` is `5` alright, but the original parameter `id` is "closed over" by `defaultID()`, which shows the original parameter `id` is still `3`, not `5`, that's why it's not updated (by the local variable). – lochiwei Oct 21 '22 at 14:52
  • 1
    It's probably a similar "half scope" as you get with `for`. (There is a great [video](https://youtu.be/Nzokr6Boeaw) about the `for` case by the way, it gives a glimpse what goes on behind the scenes and that a "scope" is an over-simplification). Welcome to the JavaScript iceberg :) – CherryDT Oct 21 '22 at 14:58
  • The phenomenon in "case 2" is called a [closure](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures) – Bqardi Oct 21 '22 at 15:10

1 Answers1

0

Your problem is not related to scope. It has to do with the re-declaration of a variable.

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/var#description:

Duplicate variable declarations using var will not trigger an error, even in strict mode, and the variable will not lose its value, unless another assignment is performed.

This is exactly what you are doing in case 2: you re-declare id with the var keyword and you assign it a new value.

The scope is simply the function scope.

These examples might clarify things concerning function scope. They illustrate the fact that OP's defaultID function simply creates a closure over id and when a new variable id is created, the reference in the defaultID function can no longer be accessed in the body of doesntUpdate.

Control example

const main = () => {
  const x = 1;

  const fn = (
    fnX,
    adder = () => { // Variable "adder" contains a function which creates
                    // its own function scope with a closure over "fnX".
      fnX = fnX + 2;
    },
    multiplier = () => { // Same here.
      fnX = fnX * 2;
    },
  ) => {
    console.log('fnX (before)', fnX);

    const added = adder();
    fnX = 5;
    const multiplied = multiplier();

    console.log('fnX (after)', fnX);
    console.log('fnX === x', fnX === x);
  };

  fn(x);
};

main();

Example with var re-declaration:

const main = () => {
  const x = 1;

  const fn = (
    fnX,
    adder = () => {
      fnX = fnX + 2;
    },
    multiplier = () => {
      fnX = fnX * 2;
    },
  ) => {
    console.log('fnX (before)', fnX);

    const added = adder();
    var fnX = 5; // fnX is now a "new" variable, it replaced the one
                  // with the same name which was created as an argument.
                  // "adder" and "multiplier" still operate on the
                  // initial variable but that variable can no longer
                  // be referenced in the body of "fn".
    const multiplied = multiplier();

    console.log('fnX (after)', fnX);
    console.log('fnX === x', fnX === x);
  };

  fn(x);
};

main();

NOTE: I created a main function so we don't have to wonder about global or module context.

Sébastien
  • 11,860
  • 11
  • 58
  • 78
  • It's not as simple as that I'm afraid. `var` redeclarations are NOT simply ignored, if you have assignment attached with your redeclaration, the assignment is **applied**, for example: `var x = 1; var x = 2; console.log(x);`, and the output will be `2`, so this is a proof that `var` redeclarations are not simply ignored, actually the assignments are still applied, and the original `var` will be updated to new valules. – lochiwei Oct 21 '22 at 22:54
  • And to my understanding, it's **not simply** the "function scope", there's a so called "[parameter scope](https://github.com/getify/You-Dont-Know-JS/blob/2nd-ed/scope-closures/apA.md#parameter-scope)" involved in the `function` declaration, but that's exactly where I got totally confused, and that's why I posted this question. – lochiwei Oct 21 '22 at 23:03
  • "the assignment is applied" Yes, that is what I said and what the documentation says. – Sébastien Oct 22 '22 at 00:37
  • " it's not simply the "function scope" ... Scope does not matter in your example. – Sébastien Oct 22 '22 at 00:38
  • Could you explain what does matter in the example? – lochiwei Oct 22 '22 at 04:58
  • @Sébastien No, but I was confused about that at first too. Check the `defaultID` function which is defined _within the parameter scope_ (not function scope) and closes over `id`. Since it still returns 3 at the end, this shows that it can't be a redeclaration because we now have two distinct variables, one with value 3 and one with value 5. – CherryDT Oct 22 '22 at 22:02
  • `var` does not do anything magical, nor does the function scope. The defaultID function does not return anything "at the end", it saves the initial value, whenever you print it: it is the initial value. `var` allows you to redeclare a variable, or to say it differently: to delcare a variable with the same name as a variable existing in the same scope, and assign it whatever value you want. – Sébastien Oct 23 '22 at 00:04
  • @CherryDT see examples – Sébastien Oct 23 '22 at 11:18
  • I'm very confused now because you just added an example that proves _my_ point, that it's not simply a redeclaration but creating a new variable in a different (inner) scope, otherwise it would be the same `id` in both parameter scope and function scope. – CherryDT Oct 23 '22 at 11:25
  • What it boils down to is this - imo very legitimate - question: If the parameter scope and the function scope are separated (as the OP's as well as your own example demonstrated), then why would `let` and `const` fail to declare a new `id` in the inner scope and complain about it already being declared? – CherryDT Oct 23 '22 at 11:26
  • Also, 'Variable "adder" contains a function which creates its own function scope with a closure over "fnX".' is correct but misleading, because `adder` never declares any variables in its own function scope. It simply closes over the `fnX` in `fn`'s parameter scope. The comment makes it sound as if the assignment of `fnX = fnX + 2` would implicitly create another variable in a different scope... – CherryDT Oct 23 '22 at 11:29
  • https://en.wikipedia.org//wiki/Gish_gallop – Sébastien Oct 23 '22 at 11:43
  • I don't see how this is related. It's a very straight forward issue. If the scopes were simply two regular separate scopes like anywhere else in JS, `var` should declare a new variable, `defaultID` should keep returning the original value, and `let` and `const` should work too. If it were the simply the same scope, `var` would just redeclare the variable, `defaultID` should return the updated value, and `let` and `const` should fail with "already declared". However, here we have a mix of the two behaviors. – CherryDT Oct 26 '22 at 09:09
  • It's related as in I don't have the time. – Sébastien Oct 26 '22 at 13:30