ES6 destructuring object assignment function parameter default value

Hi I was going through examples of object destructuring use in passing function parameters here Object Destructuring Demo

function drawES6Chart({size = 'big', cords = { x: 0, y: 0 }, radius = 25} = **{}**) {
  console.log(size, cords, radius);
 // do some chart drawing
}

 // In Firefox, default values for destructuring assignments are not yet  
 implemented (as described below). 
 // The workaround is to write the parameters in the following way:
   // ({size: size = 'big', cords: cords = { x: 0, y: 0 }, radius: radius =  
      25} = **{}**)

 drawES6Chart({
    cords: { x: 18, y: 30 },
    radius: 30
});

Can anybody let me know what is reason of using empty object assignment at the end of function parameter which I have marked in bold(embedded in double stars) above?


If you use it, and call the function with no parameters, it works:

function drawES6Chart({size = 'big', cords = { x: 0, y: 0 }, radius = 25} = {}) {
  console.log(size, cords, radius);
 // do some chart drawing
}

drawES6Chart();

if not, an error is thrown:

TypeError: can't convert undefined to object

function drawES6Chart({size = 'big', cords = { x: 0, y: 0 }, radius = 25}) {
  console.log(size, cords, radius);
 // do some chart drawing
}

drawES6Chart();

The destructuring with defaults only does its thing when you pass an object which doesn't have the respective properties. The = {} default for the whole parameter allows to not pass an (empty) object at all.

It makes drawES6Chart() equivalent to drawES6Chart({}).


You have an object with your default values, but that object is an argument too, so it needs an empty object as a default value for the first argument, which is the object with the filled in values.

function drawES6Chart({size = 'big', cords = { x: 0, y: 0 }, radius = 25} = {}) {
}

That, in pseudo code, would be:

function drawES6Chart({**first argument**} = {**default value for first argument**}) {
}

Here is a (much longer than I originally intended) description of the phenomenon you are observing from a more rigorous point of view. Why more rigorous? I wanted to investigate this question because I wasn't sure if there was some special rule regarding function default arguments, or if there was something fundamental about destructuring that I didn't understand. Turns out, it was the latter.

I'll describe my findings using pseudo-grammar that somewhat mirrors what you'll see in ECMA-262. That is my only reference.

Key Points:

There are the Destructuring Assignments and Destructuring Binding Patterns. The purpose of both is to introduce names and assign values.

Destructuring Assignment:

ObjectAssignmentPattern : '{' AssignmentPropertyList '}' = AssignmentExpression AssignmentPropertyList : AssignmentProperty [',' AssignmentProperty]

These two just state the general form of the Destructuring Assignment.

AssignmentProperty : IdentifierReference [Initializer]

This is a "default value" for a name in the LHS.

AssignmentProperty : PropertyName ':' AssignmentElement AssignmentElement : LeftHandSideExpression [Initializer]

This lets the destructuring nest recursively, but the semantics need to be defined.

Semantics

If you look at DestructuringAssignmentEvaluation, you can see who gets assigned to what. ObjectAssignmentPattern is not very interesting, it gives the basic '{' assignments '}' structure of the LHS, what's more interesting is 12.15.5.3, PropertyDestructuringAssignmentEvaluation. This shows what happens when you actually assign default values, and when you bind more deeply nested names.

AssignmentProperty : IdentifierReference [Initializer]

Step 3 is important in this algorithm, where GetV is called. In this call, it is attempting to get the value of the name that is currently being assigned to (LHS) from value (RHS). This can throw, and is why the following snippet throws:

 y = Object.defineProperty({},'foo',{get: () => {throw new Error("get foo");}})
 {foo} = y;

The next step, step 4, just evaluates the initializer if it exists and the value obtained from the RHS is undefined. For example:

 y = Object.defineProperty({},'foo',{get: () => undefined})
 {foo = 3} = y; // foo === 3

Note that this step, and the step of actually "putting" the value where it needs to go, can both throw. The next item is more tricky, and is where confusion most certainly arises:

AssignmentProperty : PropertyName ':' AssignmentElement

The semantics here are to kick the can down the road to KeyedDestructuringAssignmentEvaluation, passing the PropertyName and current value (RHS). Here's the header for its runtime semantics:

AssignmentElement : DestructuringAssignmentTarget [Initializer]

The steps of the ensuing algorithm are somewhat familiar, with a few surprises and indirections. Almost any step in this algorithm can throw, so it won't be pointed out explicitly. Step 1 is another "base of recursion," saying that if the target is not an object or array literal (e.g. just an identifier), then let lref be that (note that it doesn't have to be an identifier, just something that can be assigned to, e.g.

w = {}
{x:w.u = 7} = {x:3} // w == {u:3}

Then, an attempt to retrieve the "target value" value.propertyName is made, using GetV. If this value is undefined, an attempt is made to get the intializer value, which in step 6 is put into lref. Step 5 is the recursive call, peeling off as many layers as necessary to achieve the base case for the destructured assignment. Here are a few more examples that I think illustrate the point.

More clarifying Examples:

{x={y:1}} = {} // x == {y:1}

{x:{y=1}} = {} // error, {}.x is undefined, tried to find {}.x.y

x = 'foo'
{x:{y=1}} = {x} // x == 'foo', y == 1.
                // x doesn't get assigned in this destructuring assignment,
                // RHS becomes {x:x} === {x:'foo'} and since 'foo'.y is
                // undefined, y gets the default 1

{x:{y=1}} = {x:{y}} // error, tried to give object value {y} === {y:y} to x
                    // in RHS, but y is undefined at that point

y = 'foo'
{x:{y=1}} = {x:{y}} // y == 'foo', gave {y} === {y:y} === {y:'foo'} to x in RHS

{x:{y=1}} = {x:{y:2}} // y == 2, maybe what you wanted?

// exercises:
{x=1} = undefined         // error
{x=1} = null              // error
{x=1} = null || undefined // error
{x=1} = null | undefined  // can you guess? x == 1

Function Declarations

I actually started looking into destructuring after seeing the following code in the source for react-redux:

export function createConnect({
  connectHOC = connectAdvanced,
  mapStateToPropsFactories = defaultMapStateToPropsFactories,
  mapDispatchToPropsFactories = defaultMapDispatchToPropsFactories,
  mergePropsFactories = defaultMergePropsFactories,
  selectorFactory = defaultSelectorFactory
} = {}) {

So, the first place I started digging was:

14.1 Function Definitions

Here is a little "stack trace" trying to track down the relevant productions to get me to the binding stuff.

FunctionDeclaration

FormalParameters

FormalParameterList

FormalParameter

-> 13.3.3 Destructuring Binding Patterns

BindingElement

+SingleNameBinding

++BindingIdentifier, Initializer

+BindingPattern

+ObjectBindingPattern

+BindingPropertyList

+BindingProperty

+SingleNameBinding,

+PropertyName ':' BindingElement

Semantics: Destructuring Binding vs. Destructuring Assignment

As far as I can tell, the only difference between Destructuring Binding and Destructuring Assignment is where they can be used and how different lexical environments are handled. Destructuring Binding outside of formal parameter lists (and the like) require initializers, and Destructuring Binding is passed an environment explicitly, while assignments, which by their definition implie an initializer," get their values from the "ambience." I'd be very happy to hear why that's wrong, but here is a quick demonstration:

var {x};    // syntax error
function noInit({x}) { return x; }
            // ok
noInit()    // runtime error
noInit({})  // undefined
noInit({x:4})  // 4

function binding({x:y} = {x:y}){ return y; }
function assigning(){({x:y} = {x:y}); return y}

binding()   // error, cannot access y before initialization
assigning() // error, y is not defined
y = 0
binding()   // still error
assigning() // 0 - now y is defined

Conclusion:

I conclude the following. The purpose of destructuring binding and assignment is to introduce names into the current lexical environment, optionally assigning them values. Nested destructuring is to carve out the shape of the data you want, and you don't get the names above you for free. You can have initializers as default values, but as soon as you use them you can't carve any deeper. If you carve out a particular shape (a tree, in fact), what you attempt to bind to may have undefined leaves, but the branch nodes must match what you've described (name and shape).

Addendum

When I started this I found it helpful and interesting to see what tsc (the typescript compiler) would transpile these things into, given a target that does not support destructuring.

The following code:

function f({A,B:{BB1=7,BB2:{BBB=0}}}) {}

var z = 0;
var {x:{y=8},z} = {x:{},z};

Transpiles (tsc --target es5 --noImplicitAny false) into:

function f(_a) {
  var A = _a.A,
    _b = _a.B,
    _c = _b.BB1,
    BB1 = _c === void 0 ? 7 : _c,
    _d = _b.BB2.BBB,
    BBB = _d === void 0 ? 0 : _d;
}
var z = 0;
var _a = { x: {}, z: z },
  _b = _a.x.y,
  y = _b === void 0 ? 8 : _b,
  z = _a.z;