Advanced JavaScript Syntax
At this point, we've learned pretty much all of the major tools you'll need in your introductory programmer's arsenal, and you have all that you need to complete not only your final project, but a wide swath of applications you might want to develop.
That said, just because we know some of the most common programmatic tools, and the syntaxes for employing them, we should know that there are some convenient and concise ways to accomplish some tasks that we have yet to see.
In this lecture, we'll learn about several such "advanced" syntaxes that can be used as quality of life improvements for you as programmers, will make your code look more mature, and is generally good style for conciseness.
First-Class Functions
Recall our revelation from last week, which was that functions are objects and can therefore be assigned to variables and object properties, which motivated the development of methods in object oriented programming.
To add to our vocabulary:
Programming languages that allow functions to be assigned to variables are said to support first-class functions.
Recall that function parameters are simply local variables as well. If a language supports first-class functions, what do you think this means in terms of what we can pass as inputs to a function call (i.e., the arguments)?
We can pass functions are arguments to functions!
The utility of passing functions as arguments to functions is fascinating: we can use one function as a template for some parameterized behavior!
A function that is provided as an argument in another function, with the intent of being called within, is known as a callback.
These functional arguments are known as callbacks since it is implied that, by providing it as an input, the called function will itself call the function input.
That's a lot of words! How about an example of this behavior? Remember the forEach method supported by arrays? Let's see how those look, but in a
dumb function example rather than a method!
function forEachInArray (arr, callback) {
// [!] Note: callback called for each element
// within the given array arr, where callback
// is a parameter to this function!
for (let a of arr) { callback(a); }
}
function logIfOdd (i) { if (i % 2) { console.log(i); } }
function logIfEven (i) { if (!(i % 2)) { console.log(i); } }
let arr = [0, 1, 2, 3];
// [!] Second argument is a function in each call below!
forEachInArray(arr, logIfOdd); // logs 1, 3
forEachInArray(arr, logIfEven); // logs 0, 2
For an example of this used in practice, see the Game class' forEachKtahbject method defined in your final project!
As it turns out, this callback syntax, and the passing of first-class functions as arguments, is so common, that we'll look at a few popular tools that employ it.
Arrow Functions
Many times we may wish to write short helper functions that specify some small procedure, but would prefer not to have to pollute the global namespace with a new function definition or use the traditional (rather verbose) function definition syntax.
Small functions known as arrow functions have the following syntax wherein the expression to the right of an arrow (=>) is what gets
returned from the function:
(params) => return_expression
Suppose we return to one of our earliest functions, the hypotenuse of a right triangle's base and height.
// [X] Old syntax:
let hypotenuse = function (b, h) {
return Math.sqrt(b * b + h * h);
};
// [!] Arrow function:
let hypotenuse = (b, h) => Math.sqrt(b * b + h * h);
Already some nice saving of characters!
Parsimony is not the only use of arrow functions, however, but with this other characteristic, there is some risk as well.
The this keyword is not bound to a receiver when using arrow functions, unlike its more verbose function () {} keyword
counterpart.
What does this mean? Well, take a look at the following example with two methods implemented using the arrow vs. regular function syntax.
// Business as usual, no surprises here
let obj = {
x: 0,
// [!] this keyword receiver refers to obj
incX: function () { this.x++; }
};
obj.incX();
console.log(obj.x);
let obj = {
x: 0,
// [!] Arrow function does not bind this to obj
incX: () => this.x++
};
obj.incX();
console.log(obj.x);
This seems like a trap -- why might this be useful?
Arrow functions *not* binding this can be useful when defining *callbacks* to be passed as arguments to functions from within a method.
The result: using arrow functions from within a method definition allows us to use the containing scope's binding of this
Arrow functions can also be specified with an extended syntax for when we have a larger function body, in which case we can specify the behavior as:
(params) => {
// Function body, now requires return statement
return result;
}
To see some motivation and an extended syntax example, see the following in your final project.
Examine the callback passed to the setTimeout event in the Game class' nextRound() method in your final projects. Why
might we want this to be an arrow function rather than the traditional syntax?
Next time, we'll look at some popular built-in methods that exploit the tools we've overviewed above!