public

Declarative JavaScript Functions

The release of "EcmaScript6" introduced us to a couple of cool features. They may seem a bit daunting at first, but are definitely worth exploring. In this post

6 years ago

Latest Post Deferring decisions in Evolutionary Architecture by Tim Sommer public

The release of "EcmaScript6" introduced us to a couple of cool features. They may seem a bit daunting at first, but are definitely worth exploring. In this post I'll take a look at the ES6 "spread" syntax in  combination with the "destructuring" assignment syntax. Both will allow your JavaScript code to be more readable, more maintainable and more fun to write. Writing "declarative functions" is a first and easy step into writing better and self-explanatory code.

Functions as first class citizens

We all know this -or at least, heard someone say it-: in JavaScript, functions are regarded as "first class citizens". This means that functions are treated like any other variable.  A function can be passed as an argument to other functions, can be returned by a function and can be assigned as a value to a variable.  Let's take a look:

// assign a function as a value to variable foo
const foo = function() {
   console.log("foobar");
}

// Invoke it using the variable
foo(); //logs "foobar"
// pass a function to antoher function
function sayHello() {
   return "Hello, ";
}

function greeting(helloMessage, name) {
  console.log(helloMessage() + name);
}

// Pass "sayHello" as an argument to "greeting" function
greeting(sayHello, "JavaScript!"); //logs: "Hello, JavaScript!"

Parameters vs Arguments

In JavaScript there is a big difference between arguments and parameters. Arguments are the value you pass into a function. Parameters are the named variables inside the function that receive those values.

For example:

function myfunction(x, y) {}
    
var a = 10;
myfunction(a, a * 10);

In this snippet "a" and "a * 10" (= 100) are the arguments, "x" and "y" are the parameters that receive the argument values (10 and 100) .

In JavaScript, there is no rule for the number of arguments to match the number of parameters.
If you pass more arguments than parameters, the values are passed in but you can only access them in a few ways (for example using the "arguments" object).  
If you pass fewer arguments than parameters, each unmatched parameter gets an undefined value. The parameter will be present and available in the scope of the function, but it will start  out with the empty undefined value.

As of ES6 you can now combine a few principles to make your code a bit safer and more readable. And that's what this post is about. To solve a set of common JavaScript problems we (probably) all have come across in our careers. Let's take a look at three issues I'll address with this post:

  1. Calling functions with less arguments than parameters.
  2. Calling functions with more arguments than parameters.
  3. Named arguments are not required to be the same as named parameters.

We should always strive for declarative, self-explanatory code!

Default values for parameters

Parameters can declare default values to get rid of the first undefined problem! In a case where there are less arguments than parameters (and thus an undefined value is passed in), you can use the default assignment expression:

function myFunction(a = 10) {
	console.log(a);
}
    
myFunction(); //10
myFunction(undefined); //10
myFunction(null); //null
myFunction(100); //100

Defaulting parameters gets rid of the undefined problem. Be careful though, this can lead to more complexity. Especially in terms of readability. But, it's a good place to start!

Function Input

But what if you are calling a function with more arguments than there are parameters declared? Well, first things first, please don't ;)!
I hope it is safe to say that occurrences of this problem are going to be very rare. In most cases it would be better to try and figure out a better design for the interaction with your function. If nothing else works, at least name the extra argument, even if it's use is very exceptional.

You can always use the arguments JavaScript object to figure out how many arguments are passed into a function. But using it brings along problems of its own, which far outreaches the boundaries of this post. I'll leave you with this: avoid using the arguments object whenever possible.

But what if you do find yourself in need of a way to get all the arguments passed into a function? Well, ES6 to the rescue!

The ... ("spread", "rest" or "gather") operator!

If we declare our function with the "..." operator we can avoid using the arguments object altogether:

function myFunction( a, b, c, ...args ) { console.log( x, y, z, args ); }

The "...args" parameter is an ES6 declarative form that tells the engine to collect (or gather) all remaining arguments (if any) and put them all in a real array named args. This args object will always be an array, even if its empty. But it will not include values that are assigned to the a, b and c parameters. Let's take a look!

myFunction();                //undefined undefined undefined []
myFunction(1, 2, 3);         //1 2 3 []    
myFunction(1, 2, 3, 4);      //1 2 3 [4]    
myFunction(1, 2, 3, 4, 5);   //1 2 3 [4, 5]   

If you really need to design a function that should be able to process more arguments than parameters, this is the way to go.

Array of arguments

Just a side-note, if you want to pass in an array of values things can become a little strange:

function myFunction(...args) { console.log( args[3] ); }
var arr = [ 1, 2, 3, 4, 5 ];
myFunction( ...arr );      //4
myFunction(arr);           //undefined

In this context the spread operator has the opposite behavior. In a parameter list it gathers arguments together. In an argument list, as in the sample above, it spreads them out. So, the values of arr is actually spread out as individual arguments.

By the way, multiple values and "..." spreading can be interleaved:

var arr = [ 2 ];
myFunction( 1, ...arr, 3, ...[4,5] ); // 4

The "..." operator's behavior changes in different contexts. Which is why it makes reading code that uses it sometimes a bit more difficult.
In a value-list position, it spreads. In an assignment position it gathers or collects. But more on that later.

Named parameters

So we have solved all but one problem: "Named arguments are not required to have the same named parameter". A couple of ES6 features we looked at will now come together. But first, let's take a look at the "destructuring assignment".

The destructuring assignment syntax is a JavaScript expression that makes it possible to unpack values from arrays, or properties from objects, into distinct variables.

var a, b, rest;
[a, b] = [10, 20];
    
console.log(a);     //10
console.log(b);     //20
    
[a, b, ...rest] = [10, 20, 30, 40, 50];
    
console.log(rest);  //[30,40,50]

So we now have a little more control over what is passed into a and b. We can use this approach to assign and create objects:

({ a, b } = { a: 10, b: 20 });
console.log(a);          // 10
console.log(b);          // 20
    
({a, b, ...rest} = {a: 10, b: 20, c: 30, d: 40});
console.log(a);          // 10
console.log(b);          // 20
console.log(rest);       // {c: 30, d: 40}
    
({ a, b } = { 10, 20 }); //error

As far as declarative syntax, this provides us with a good start to look into named arguments! Let's take the "myFunction" function again:

function myFunction( {a,b} = {} ) { console.log( a, b ); }
myFunction( { b: 3 } ); // undefined 3
myFunction( 2,3 ); // undefined undefined

We pass in an object as the single argument, which is destructured into two separate parameter variables a and b. These are assigned with the values of the corresponding property names from the object passed in. It does not matter that the a property is not present on the object. It just ends up as a variable with undefined, like you’d expect.

But it's important you pay attention to the object being passed into "myFunction". With a normal call like myFunction(undefined, 3), position is used to map from argument to parameter. The value 3 is placed in second position, which is then assigned to the b parameter. I hope it is clear that this can be error prone.
With this new kind of call-site where parameter destructuring is involved, a simple object-property indicates which parameter the argument value 3 should be assigned to. This leads to less bugs, more readable code at virtually no cost!

By default, JavaScript does not have named arguments, but parameter object destructuring is the next best thing. Named arguments are much more flexible, and attractive from a readability perspective, especially when the function in question can take three, four, or more inputs.

Using spread operator in object literals

It's worth noting that you can use the spread operator to copy enumerable properties from a provided object onto a new object.

function myFunction( {a,b} = {} ) { console.log( a, b ); }
    
var obj1 = { foo: 'bar', a: 42 };    
var obj2 = { foo: 'baz', a: 100, b: 100 };
    
myFunction(obj1);                           //42 undefined
myFunction(obj2);                           //100 100
    
var mergedObj = { ...obj1, ...obj2 };       // Object { foo: "baz", x: 100, y: 100 }
    
myFunction(mergedObj);                      //100 100
myFunction({ ...obj1, ...obj2 });           //100 100

Summary

JavaScript is a cool language. I -and many others- love it!
But there are challenges in writing readable and maintainable code, especially in large-scale applications. Using the concepts described above allow you to do some pretty cool things! Levering the features of ES6 you can write better and more understandable code. And that should be the goal of any good programmer.

Hope you liked the read and don't forget to leave a comment!

This post was inspired by some concepts introduced in the book "Functional-Light JavaScript" by Kyle Simpson. It's a great read and I fully recommend you taking a look yourself! You can find the eBook here.

Photo by Irvan Smith on Unsplash

Tim Sommer

Published 6 years ago

Comments?

Leave us your opinion.