Yes, you are right. There are three different scopes involved here (one for the first parameter, one for the second, and one for the body).
However, after the parameters were initialized (in their own scope) they get copied to a new lexical environment (in which the body will be executed then) (can be found in 9.2.15 of the spec).
That means that the parameter names not only exist in the scope of the parameter, but also in the scope the body is evaluated in, thus using the same name inside of the body is a redeclaration of the variable, resulting in an error (with let
/ const
).
Here is a walkthrough of the spec:
When a function gets parsed, it will create a function object, which contains some internal properties:
[[Environment]]: Thats a reference to the outer scope, so that you can access variables of the outer scope inside a function (this also causes the closuring behaviour, [[Environment]] might reference an environment that is not active anymore).
[[FormalParameters]]: The parsed code of the parameters.
[[ECMAScriptCode]]: The code of the functions body.
Now when you call a function (9.2.1 [[Call]]), it'll allocate an environment record on the callstack, and will then
Let result be OrdinaryCallEvaluateBody(F, argumentsList).
call the function. Thats where 9.2.15 comes in. First of all, it will declare all parameters in the functions body environment:
[Initialize local helper variables]
21. For each String paramName in parameterNames, do
i. Perform ! envRec.CreateMutableBinding(paramName, false).
[Rules for initializing the special "arguments" variable]
Then it will initialize all the parameters. Parameters are really complicated, cause there are also rest parameters. Therefore arguments have to be iterated to possibly turn them into arrays:
24. Let iteratorRecord be CreateListIteratorRecord(argumentsList)
25. If hasDuplicates is true, then
a. Perform ? IteratorBindingInitialization for formals with iteratorRecord and undefined as arguments.
26. Else,
a. Perform ? IteratorBindingInitialization for formals with iteratorRecord and env as arguments.
Now IteratorBindingInitialization
is defined in 13.3.3.8:
It'll basically evaluate the default value, and will initialize a binding in the current environment.
When all parameters are initialized, the functions bodys environment can be prepared. As mentioned in the comment you quoted, if there are no parameter initializers no new environment is required. If there is however a default value somewhere, a new environment will be created.
27. If hasParameterExpressions is false, then
[...]
28. 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.
Then a "new scope" gets created, in which the body gets evaluated:
b. Let varEnv be NewDeclarativeEnvironment(env).
c. Let varEnvRec be varEnv's EnvironmentRecord.
d. Set the VariableEnvironment of calleeContext to varEnv.
Then all the variables and parameters of the function get declared and initialized in that scope, meaning that all parameters get copied:
f. For each n in varNames, do
2. Perform ! varEnvRec.CreateMutableBinding(n, false).
3. If n is [a regular variable declared with "var"], let
initialValue be undefined.
4. Else, [if it is a parameter]
a. Let initialValue be ! envRec.GetBindingValue(n, false)
5. Call varEnvRec.InitializeBinding(n, initialValue).
[Other rules for functions in strict mode]
31. [varEnv gets renamed to lexEnv for some reason]
After that, all inner variables with let
/ const
get declared (but not initialized, they will be initialized when the let
gets reached).
34. Let lexDeclarations be the LexicallyScopedDeclarations of code.
35. For each element d in lexDeclarations, do
a. NOTE: A lexically declared name cannot be the
same as a function/generator declaration, formal
parameter, or a var name. Lexically declared
names are only instantiated here but not initialized.
b. For each element dn of the BoundNames of d, do
i. If IsConstantDeclaration of d is true, then
1. Perform ! lexEnvRec.CreateImmutableBinding(dn, true).
ii. Else,
1. Perform ! lexEnvRec.CreateMutableBinding(dn, false).
Now as you can see, CreateMutableBinding
gets called here, and as specified in 8.1.1.1.2 ...
The concrete Environment Record method CreateMutableBinding for declarative Environment Records creates a new
mutable binding for the name N that is uninitialized. A binding must not already exist in this Environment Record.
So as the parameter was already copied into the current environment, calling CreateMutableBinding
with the same name will fail.