10

Looking at this simple code which uses Lazy Expressions:

var x = 1;

function foo(x = 2, f = () => x) {
  var x = 5;
  console.log(f())
}

foo()

The output here is 2.

I must say that I thought it should output 5. However - this would've been logical if f was closing over the parameter list scope - if it had a scope.

Because looking at this other example (which a bit related) :

var x = 5;
var f = function() {
  return x;
}
x = 1
f();
console.log(x)

This will output 1. (Which is the expected result.).

Question

What's actually going here with the parameter list scope ? is there any scope here at all ?(at the parameter list)

I didn't find scope related info in the docs.

Royi Namir
  • 144,742
  • 138
  • 468
  • 792
  • 3
    Interesting. Babel of course transpiles this down to move the declarations inside the body of the function, and so produces `5` as a result - which, would be a bug. – CodingIntrigue Oct 04 '17 at 07:26
  • Your examples don't match. You're outputting the return value of `f` in the first, `x` after `f` execution in the second. – Serge K. Oct 04 '17 at 07:26
  • 1
    @NathanP. I know that the example doesn't match. I was specifying that the other example is understood to me while the first one ( which i don't know if uses closure) - is not understood to me. – Royi Namir Oct 04 '17 at 07:27
  • 1
    What is "Lazy Expressions"? The code at first example uses default parameters. – guest271314 Oct 04 '17 at 07:31
  • @guest271314 I know. jsut like C#. But here ( in the link) it says also lazy expressions: https://frontendmasters.com/courses/es6-right-parts/lazy-expressions/ – Royi Namir Oct 04 '17 at 07:32
  • 1
    Removing the `var` prints `5` – Suren Srapyan Oct 04 '17 at 07:33
  • The link is not to the specification. – guest271314 Oct 04 '17 at 07:34
  • 1
    FWIW, if you change `var x = 5` to `let x = 5` in the first example, it errors because it has already been declared, so to answer your formal question, no, parameter lists do not have their own scope. – Patrick Roberts Oct 04 '17 at 08:03
  • @PatrickRoberts Yet you can shadow parameter bindings with `var` declarations without affecting closures (see the accepted answer). Feels like a pseudo-scope. –  Oct 04 '17 at 09:03

3 Answers3

5

Function parameters have scope.

In you first example you allocate a new x variable, which is why it doesn't overwrite:

//Global x
var x = 1;

function foo(x = 2 /* Local scope x */ , f = () => x /* Local scope x bound to new function scope */ ) {
  /* new local scope x. If you removed the "var", this would overwrite localscope x */
  var x = 5;
  /* All 3 x's accessed */
  console.log(f(), x, window.x)
}

foo()

var x = 1;
function foo(x = 2, f = () => x) {
  x = 5;
  console.log(f(), x, window.x)
}
foo()

EDIT 1 - TypeScript

As answer to the comments. TypeScript compiles this ES6 version:

//Global x
var x = 1;

function foo(x = 2 /* Local scope x */ , f = () => x /* Local scope x bound to new function scope */ ) {
  /* new local scope x. If you removed the "var", this would overwrite localscope x */
  var x = 5;
  /* All 3 x's accessed */
  console.log(f(), x, window.x)
}

foo()

Into this:

//Global x
var x = 1;
function foo(x /* Local scope x */, f /* Local scope x bound to new function scope */) {
    if (x === void 0) { x = 2; } /* Local scope x */
    if (f === void 0) { f = function () { return x; }; } /* Local scope x bound to new function scope */
    /* new local scope x. If you removed the "var", this would overwrite localscope x */
    var x = 5;
    /* All 3 x's accessed */
    console.log(f(), x, window.x);
}
foo();

It does this because older browsers don't support parameter declaration, but it messes with the scope if compared to the straight ES6 version.

Emil S. Jørgensen
  • 6,216
  • 1
  • 15
  • 28
  • Typescript shows 5.... wonder why. Also I didn't understand why removing the var changes the result. X is under closure and captured so why is it being overwritten ? – Royi Namir Oct 04 '17 at 09:05
  • Think of it in a 3 part way. You have your window scope on top, your parameter scope in the middle and your function scope on the bottom. Variables inherited from a higher scope is superseded by a lower scope `var`, but if no `var` declaration exists we travel up the chain until we find one or hit `window`. Therefore adding a variable declaration in the lowest scope will only affect methods that use that variable **after** its declaration. TypeScript takes parameter declaration into the function scope, which explains your result. – Emil S. Jørgensen Oct 04 '17 at 09:58
4

This is an interesting question.

It's best to refer Ecmascript 2017 specifications to understand how exactly the argument binding mechanism work.

When a function is being defined there are one or two Environment Records in action. The bindings set by the Environment Record(s) differ depending on whether the arguments have a default value or not. If the argument(s) have default values then, 2 Environment Records are in action. One for the parameter instantiations and one for the body declaration (such as variables, inner functions etc..).

Obviously when you do like;

function(x = 2, y = x){
  ...
}

there is a pre-assignment function at work and it has to have it's own context. So in case of;

function foo(x = 2, f = () => x) {
  var x = 5;
  console.log(f())
}

x gets under closure at the time of the function parameters' definition.

So let's read the relevant part of the ECMA 2017 specs where it says;

9.2.12 FunctionDeclarationInstantiation(func, argumentsList)

When an execution context is established for evaluating an ECMAScript function a new function Environment Record is created and bindings for each formal parameter are instantiated in that Environment Record. Each declaration in the function body is also instantiated. If the function's formal parameters do not include any default value initializers then the body declarations are instantiated in the same Environment Record as the parameters. If default value parameter initializers exist, a second Environment Record is created for the body declarations. Formal parameters and functions are initialized as part of FunctionDeclarationInstantiation. All other bindings are initialized during evaluation of the function body.

Also we are given an algorithm in detail how to implement this functionality if we ever need to sit and code our own JS engine. Step 27.a is interesting.

27 Else,

a. NOTE A separate Environment Record is needed to ensure that closures created by expressions in the formal parameter list do not have visibility of declarations in the function body.

Community
  • 1
  • 1
Redu
  • 25,060
  • 6
  • 56
  • 76
  • If they have a separate scope, then i do not understand why removing var changing the result – Royi Namir Oct 04 '17 at 16:21
  • @Royi Namir If you remove `var` then `x` will point to the `x` at the variable declared at the arguments in which case you will be reassigning it with `x = 5;` instruction. Hence `f` will print `5`. If you declare `x` with `var` in the function body it will override the `x` defined in arguments but the function will keep the closed value. – Redu Oct 04 '17 at 16:46
  • 1
    When we assume that the scope of the formal parameter list encloses the scope of the function body, it is indeed odd that declaring `x` with a `let` binding (instead of `var`) causes an error. –  Oct 04 '17 at 16:57
  • I guess that the function body doesn't establish a block scope, but a different kind of scope, which doesn't affect `let` bindings. –  Oct 04 '17 at 17:01
  • @ftor That's indeed a good point. It throws an `'x' has already been declared` error. I understand `let`'s block scoping allows only function body and this conflicts with the scoping of argument level declarations with default values (two variables with same identifier at the same time). – Redu Oct 04 '17 at 17:14
  • Thank you for the official info. But still - is this a scope? They don't mention it but environment – Royi Namir Oct 06 '17 at 05:47
  • @Royi Namir You are welcome. [**8.1.1.1Declarative Environment Records**](https://www.ecma-international.org/ecma-262/#sec-declarative-environment-records) : Each declarative Environment Record is associated with an ECMAScript program scope containing variable, constant, let, class, module, import, and/or function declarations. A declarative Environment Record binds the set of identifiers defined by the declarations contained within its scope. – Redu Oct 06 '17 at 08:10
  • @Royi Namir As I understand when the arguments need any functional action such as default value assignment, rest operators or destructuring etc there is a function running to act upon this environment record (this is where the closure happens). I believe this is done even before the function body gets started being defined. – Redu Oct 06 '17 at 08:14
  • So that closure is the parent closure of the function body? Because otherwise removing var - wouldn't have affect the value. Right? – Royi Namir Oct 06 '17 at 08:16
  • @Royi Namir At this moment i would like to remind the first comment of mine above. *If you declare `x` with `var` in the function body it will override the `x` defined in arguments but the function will keep the closed value.* Override here means "delete". So the `x` as defined among the arguments will be deleted and another `x` defined with `var` in the function body will take it's place.. however the `_ => x` function will keep the closed value of course. This is probably done in lower level and not with typical JS closures. May be we could mimic it like; https://repl.it/MMYd – Redu Oct 06 '17 at 09:49
-2

The function parameters have the same scope, and the latter parameters can be set using the previous adjacent parameter values.

guest271314
  • 1
  • 15
  • 104
  • 177
  • Even when the parameter is a function which is not executed until later? – CodingIntrigue Oct 04 '17 at 07:28
  • im not interesting about explaining the second example. – Royi Namir Oct 04 '17 at 07:29
  • @CodingIntrigue Yes. – guest271314 Oct 04 '17 at 07:29
  • 1
    @RoyiNamir Not sure what you mean? The Answer addresses the first example – guest271314 Oct 04 '17 at 07:30
  • @CodingIntrigue `function foo (x=2 ,y = x * 10, z = () => x * y){ var x = 5; alert(z()) } foo()`, see also [Can we set persistent default parameters which remain set until explicitly changed?](https://stackoverflow.com/questions/43466657/can-we-set-persistent-default-parameters-which-remain-set-until-explicitly-chang), [Why does .then() chained to Promise.resolve() allow const declaration to be reassigned?](https://stackoverflow.com/questions/45380637/why-does-then-chained-to-promise-resolve-allow-const-declaration-to-be-reas) – guest271314 Oct 04 '17 at 07:35