This article is intended for a person taking his first tentative steps on the thorny path of learning JavaScript. Despite the fact that in the courtyard of 2018, I use the syntax of ES5, so that the article was understandable for young Padawans who are taking a course "JavaScript, level 1" on HTML Academy.One of the features that distinguish JS from many other programming languages is that the function in this language is “first class object”. Or, in Russian, a function is a meaning. Same as number, string or object. We can write a function in a variable, we can put it in an array or in a property of an object. We can even add up two functions. In fact, nothing meaningful will come of it, but as a fact we can!
function hello(){}; function world(){}; console.log(hello + world);
The most interesting thing is that we can create functions that operate on other functions — taking them as arguments or returning them as values. Such functions are called
higher order functions . And today we, girls and boys, will talk about how to adapt this opportunity to the needs of the national economy. Along the way, you will learn more about some useful features of functions in JS.
Pipeline
Suppose we have a piece with which we need to make a lot of pieces. Let's say a user uploaded a text file that stores data in JSON format, and we want to process its contents. First, we need to cut off extra white space characters that could “grow” along the edges as a result of user actions or the operating system. Then check that there is no malicious code in the text (who knows, these users). Then convert from text to object using the
JSON.parse
method. Then remove the data we need from this object. And in the end - send this data to the server. It turns out something like this:
')
function trim(){}; function sanitize(){}; function parse(){}; function extractData(){}; function send(){}; var textFromFile = getTextFromFile(); send(extractData(parse(sanitize(trim(testFromFile))));
Looks like, so-so. In addition, you probably did not notice that there is not enough one closing bracket. Of course, this would be prompted to you by the IDE, but still there is some problem. To solve it, not so long ago a
new operator was proposed
|> . In fact, it is not new, but honestly borrowed from functional languages, but this is not the point. Using this statement, the last line could be rewritten as follows:
textFromFile |> trim |> sanitize |> parse |> extractData |> send;
The |> operator takes its left operand and passes it to the right operand as an argument. For example,
"Hello" |> console.log
equivalent to
console.log("Hello")
. This is very convenient for cases when several functions are called in a chain. However, before the introduction of this operator, a long time will pass (if this proposal is accepted at all), but we must live somehow now. Therefore, we can write our own
bicycle function imitating this behavior:
function pipe(){ var args = Array.from(arguments); var result = args.shift(); while(args.length){ var f = args.shift(); result = f(result); } return result; } pipe( textFromFile, trim, sanitize, parse, extractData, send );
If you are a beginner javaskriptist (javaskriptr? Javaskriptchik?), You may find it incomprehensible the first line of the function. It's simple: inside the function, we use the
arguments keyword to access an array-like object containing all the arguments passed to the function. This is very convenient when we do not know in advance how many arguments she will have. A massive object is like an array, but not quite. Therefore, we convert it to a normal array using the
Array.from
method. Further code, I hope, is already sufficiently readable: we start from left to right to extract elements from an array and apply them to each other in the same way as the |> operator would.
Logging
Here is another example, close to real life. Suppose we already have a function
f
that does ... something useful. And in the process of testing our code, we want to know more about exactly how
f
does this. At what moments it is called, which arguments are passed to it, which values are returned.
Of course, with each function call we can write this:
var result = f(a, b); console.log(" f " + a + " " + b + " " + result); console.log(" : " + Date.now());
But, firstly, it is quite cumbersome. And secondly, it is very easy to forget about it. One day we will write simply
f(a, b)
, and since then the darkness of ignorance settles in our minds. It will expand with each new
f
call that we know nothing about.
Ideally, I would like the logging to occur automatically. That each time at a call
f
all things necessary for us were written to the console. And, fortunately, we have a way to do this. Meet the new feature of higher order!
function addLogger(f){ return function(){ var args = Array.from(arguments); var result = f.apply(null, args); console.log(" " + f.name + " " + args.join() + " " + result + "\n" + " : " + Date.now()); return result; } } function sum(a, b){ return a + b; } var sumWithLogging = addLogger(sum); sum(1, 2);
The function accepts the function and returns the function that calls the function that was passed to the function when the function was created. Sorry, I could not refrain from writing this. Now in Russian: the
addLogger
function creates a kind of wrapper around the function passed to it as an argument. Wrapping is also a function. When called, it collects an array of its arguments in the same way as we did in the previous example. Then, using the
apply method, it calls the "wrapped" function with the same arguments and remembers the result. After that, the wrapper writes everything to the console.
Here we have a classic case of a man-in-the-middle attack. If instead of
f
use a wrapper, then from the point of view of the code that uses it there is practically no difference. The code can assume that it communicates with
f
directly. In the meantime, the wrapper reports everything to the comrade major.
Eins, zwei, drei, vier ...
And one more task, close to practice. Suppose we need to enumerate some entities. Every time a new entity appears, we get a new number for it, one more than the previous one. To do this, we start the function of the following form:
var lastNumber = 0; function getNewNumber(){ return lastNumber++; }
And then we have a new kind of entity. Say, before that we numbered bunnies, and now rabbits have also appeared. If you use one function for those and for others, then each number issued to rabbits will make a “hole” in the number of numbers given to bunnies. So, we need the second function, and with it the second variable:
var lastHareNumber = 0; function getNewHareNumber(){ return lastHareNumber++; } var lastRabbitNumber = 0; function getNewRabbitNumber(){ return lastRabbitNumber++; }
You feel that this code smells bad? I would like to have something better. Firstly, I would like to be able to declare such functions less verbose, without duplicating the code. And secondly, I would like to somehow “pack” the variable used by the function into the function itself so as not to clutter the namespace once again.
And then a man rushes in, familiar with the concept of the PLO, and says:- Elementary, Watson. It is necessary to make the number generators are not functions, but objects. Objects are intended to store functions that work with data, along with these same data. Then we could write something like:
var numberGenerator = new NumberGenerator(); var n = numberGenerator.get();
To which I will answer:
- To be honest, I totally agree with you. And in principle, this is a more correct approach than what I now propose. But we have an article about functions here, and not about OOP. So could you shut up for a while and let me finish?
Here we will (surprise!) Again a higher order function.
function createNumberGenerator(){ var n = 0; return function(){ return n++; } } var getNewHareNumber = createNumberGenerator(); var getNewRabbitNumber = createNumberGenerator(); console.log( getNewHareNumber(), getNewHareNumber(), getNewHareNumber(), getNewRabbitNumber(), getNewRabbitNumber(), );
And here some people may have a question, perhaps even in an obscene form: what the hell is going on? Why do we create a variable that is not used in the function itself? How does the inner function refer to it if the outer has completed its execution long ago? Why do two functions created by accessing the same variable get a different result? All these questions have one answer -
closure .
Each time the
createNumberGenerator
function is
createNumberGenerator
, the JS interpreter creates a magic thing called the execution context. Roughly speaking, this is an object in which all variables declared in this function are stored. We cannot access it as a regular javascript object, but nevertheless it is.
If the function was “simple” (say, the addition of numbers), then after the end of its work, the execution context is not needed by anyone. Do you know what happens with unnecessary data in JS? They are devoured by an insatiable demon named Garbage Collector. However, if the function was “not simple”, it may happen that its context is still needed by someone even after this function has been executed. In this case, the Garbage Collector spares him, and he remains hanging somewhere in memory so that those who need it can still have access to it.
Thus, the function returned by
createNumberGenerator
will always have access to its own copy of the variable
n
. You can think of it as the Bag of Holding of D & D. You put your hand in your bag, you fall into a personal interdimensional “pocket” where you can keep everything you want.
Debounce
There is such a thing as "bounce elimination." This is when we do not want any function to be called too often. Suppose there is some kind of button, clicking on which starts the “expensive” (long, or devouring a lot of memory, or the Internet, or sacrificing virgins) process. It may happen that an impatient user starts clicking on this button with a frequency of more than ten hertz. At the same time, the aforementioned process is of such a nature that there is no point in launching it ten times in a row, because the final result will not change. It is then that we apply the "bounce removal".
Its essence is very simple: we perform the function not immediately, but after some time. If before the time passes, the function was called again, we "reset the timer." Thus, the user can click a button at least a thousand times - a virgin for the sacrifice will need only one. However, fewer words, more code:
function debounce(f, delay){ var lastTimeout; return function(){ if(lastTimeout){ clearTimeout(lastTimeout); } var args = Array.from(arguments); lastTimeout = setTimeout(function(){ f.apply(null, args); }, delay); } } function sacrifice(name){ console.log(name + " * *"); } function sacrificeDebounced = debounce(sacrifice, 500); sacrificeDebounced(""); sacrificeDebounced(""); sacrificeDebounced("");
In half a second Lena will be sacrificed, and Katya and Sveta will survive thanks to our magical function.
If you have carefully read the previous examples, you should understand well how everything works here. The wrapper function created by
debounce
starts the delayed execution of the original function using
setTimeout . At the same time, the timeout identifier is stored in the lastTimeout variable, accessible to the wrapper due to the closure. If the timeout identifier is already in this variable, the wrapper cancels this timeout with
clearTimeout . If the previous timeout has already been executed, then nothing happens. If not, so much the worse for him.
On this, perhaps, I finish. I hope you learned a lot of new things today, and most importantly, you understood everything you learned. Until new meetings.