Understand JavaScript Functions
Learning Objectives
After completing this unit, you'll be able to:
- Recognize the fat arrow syntax for functions.
- Describe the scope used with the
this
keyword. - Explain why defining optional parameters in ES6+ results in cleaner code.
- Describe the different uses for the '
...
' operator.
The Trouble with This
You're probably familiar with defining functions like this:
let result = function (i,j) { return i+j; } console.log(result(2,3));
Executing that bit of code displays 5 in the console. ES6 introduced a shorter way to define functions using what is called arrow functions. If you are coming from another language such as C#, then arrow functions will look pretty similar to something you know as lambda expressions. Using the fat arrow symbol ( =>
), you can now create the same function using code like this:
let result = (i,j) => i+j; console.log(result(2,3));
All we have done here is remove the function
and return
keywords and used the new fat arrow symbol instead. The parentheses are even optional when there is only one parameter, and you only need the curly braces when you have more than one expression. Just remember that if you do include the curly braces, the return
keyword is required.
Arrow functions result in less code and remove some of the confusion when dealing with the this
keyword, especially when nested functions are involved. Functions have a special variable called this
, often referred to as the “dynamic this,” which refers to the object used to invoke the function.
The dynamic nature of this
causes problems in certain situations. Take for example, the following code, in which a function is called using an object:
let message = { hello : 'Hello', names : ['Sue', 'Joe'], showMessage: function() { this.names.forEach(function(name) { console.log(this.hello + ' ' + name); }); } } message.showMessage();
Executing this code in PlayCode should display two messages in the console: “undefined Sue” and “undefined Joe.” The variable named hello
cannot be referenced inside the nested function because the JavaScript interpreter thinks it is an unsupplied function argument. It has no scope inside of the nested function. Referencing the this
keyword inside the nested function just refers to the scope in which the object was invoked, which in this case is global, meaning the variable hello
does not exist.
To get around this, you can do something like the following, in which you add a new variable inside of the showMessage
function named self
. The self
variable references what is known as the “lexical scope,” because it was defined within the showMessage
function.
let message = { hello : 'Hello', names : ['Sue', 'Joe'], showMessage: function() { let self = this; this.names.forEach(function(name) { console.log(self.hello + ' ' + name); }); } } message.showMessage();
Executing this code displays the right messages in the console: “Hello Sue” and “Hello Joe.” But using the self
variable is a workaround.
The arrow function that ES6 introduced has the lexical scope built in. So, we can replace the code above with the following and it will work as expected, without having to declare an extra variable to represent this
.
let message = { hello : 'Hello', names : ['Sue', 'Joe'], showMessage: function() { this.names.forEach(name => { console.log(this.hello + ' ' + name); }); } } message.showMessage();
Better Parameter Handling
Prior to ES6, parameter handling in functions was tedious. To ensure that your code ran as expected, you often had to add manual checks within the function for any optional parameters. For example, consider a function that has two parameters. Since the user may not enter the second parameter, you'd need to add a line of code to the function to check whether they did.
function helloMessage (param1, param2) { param2 = param2 || 'World'; return param1 + ' ' + param2; } console.log(helloMessage('Hello')); //Displays "Hello World"
ES6 offers better ways to handle function parameters. You can now specify default parameter values via an equal sign ( =
) in the parameter list. For the helloMessage
function, the second parameter is optional, but you don't need that extra line of code inside the function.
function helloMessage (param1, param2 = 'World') { return param1 + ' ' + param2; } console.log(helloMessage('Hello')); //Displays "Hello World"
You can even simulate named parameters by utilizing the object destructuring syntax you learned about in the last unit. For example, consider this function with two parameters.
function showMessage(who, {p1 = "Hello", p2 = "World"} = {}) { console.log(who + ' says ' + p1 + ' ' + p2); } showMessage("Trailhead"); //Displays "Trailhead says Hello World"
The second parameter is just an object that is specified with the destructuring syntax. But notice how there is an equal sign followed by empty curly braces. This enables you to call the function without parameters. And this is important because without that equals sign, you would get a TypeError when trying to run that last function without all the parameters.
But what if you had a function with an unknown number of arguments? In ES5 you could use the arguments variable. But the arguments variable was a symbol, not an array, and using it was not easy.
ES6 introduced a better way to access these remaining, zero or more, unknown arguments using rest. Get it? “Rest,” as in “give me the rest of the arguments.” Rest arguments are indicated with three dots ( ...
) and they can appear only at the end of the argument list. For example:
function showContact (firstName, lastName, ...titles) { console.log(firstName + ' ' + lastName + ', ' + titles[0] + ' and ' + titles[1]); } showContact('Sue', 'Johnson', 'Developer', 'Architect');
Executing this code would result in the message, “Sue Johnson, Developer and Architect” displayed in the console. If you left off the last parameter in the function call, you would get “Sue Johnson, Developer and undefined” instead.
Two Uses for the Same Thing?
Now that you understand how rest parameters work, here's a question: What do you think this code prints to the console?
let array1 = ['one', 'two']; let array2 = ['three', 'four']; array1 = [...array1, ...array2]; console.log(...array1);
Did you guess, “one”, two”, “three”, “four”?
If you did, then you understand how the spread operator works. And now are you thinking, “Wait, what did you say. Spread? I thought it was called rest.”
The three dots ( ...
) operator has two uses. As the rest operator, it is used to gather up all the remaining arguments into an array. But as the spread operator, it allows an iterable such as an array or string to be expanded in places where zero or more arguments or elements are expected, or an object to be expanded in places where zero or more key-value pairs are expected. It is just either expanding or collapsing that iterable. Arrays can be spread into objects but objects can't be spread into arrays. Make sense? Check out the Resources for more examples.
Resources
- MDN Web Docs: Arrow functions in the mdn web docs
- sitepoint: ES6 Arrow Functions: Fat and Concise Syntax in JavaScript
- MDN Web Docs: Default Parameters
- Mozilla Hacks: ES6 In Depth: Rest parameters and defaults
- MDN Web Docs: Rest parameters
- MDN Web Docs: Spread syntax