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.