📜 ⬆️ ⬇️

Expressive JavaScript: Functions

Content




People believe that computer science is an art for geniuses. In reality, the opposite is true - just a lot of people do things that stand on each other, as if making up a wall of small pebbles.

Donald whip
')
You have already seen calls to functions such as alert . Functions are bread and butter programming on javascript. The idea of ​​wrapping a piece of a program and calling it as a variable is very much in demand. It is a tool for structuring large programs, reducing repetitions, assigning names to subprograms, and isolating subprograms from each other.

The most obvious use of functions is to create a new dictionary. Coming up with words for ordinary human prose is a bad form. In a programming language, this is necessary.

The average adult Russian-speaking person knows about 10,000 words. A rare programming language contains 10,000 built-in commands. And the vocabulary of a programming language is defined more clearly, so it is less flexible than a human one. Therefore, we usually have to add our own words to it in order to avoid unnecessary repetitions.

Function definition


The definition of a function is the usual definition of a variable, where the value that the variable receives is a function. For example, the following code defines the variable square, which refers to a function that counts the square of a given number:

 var square = function(x) { return x * x; }; console.log(square(12)); // → 144 


A function is created by an expression starting with the function keyword. Functions have a set of parameters (in this case, only x), and a body containing instructions that must be executed when calling a function. The body of the function is always enclosed in braces, even if it consists of a single instruction.

A function may have several parameters, or none at all. In the following example, makeNoise does not have a list of parameters, and power has two of them:

 var makeNoise = function() { console.log("!"); }; makeNoise(); // → ! var power = function(base, exponent) { var result = 1; for (var count = 0; count < exponent; count++) result *= base; return result; }; console.log(power(2, 10)); // → 1024 


Some functions return a value, like power and square, others do not return, like makeNoise, which produces only a side effect. The return defines the value returned by the function. When the program's processing reaches this instruction, it immediately exits the function, and returns this value to the place in the code from which the function was called. return without an expression returns the value undefined .

Parameters and scope


The parameters of the function are the same variables, but their initial values ​​are specified when the function is called, and not in its code.

An important property of functions is that the variables created inside the function (including parameters) are local to this function. This means that in the example with power, the result variable will be created each time the function is called, and its separate incarnations are not related to each other.

This locality of variables applies only to parameters and variables created inside functions. Variables defined outside of any function are called global because they are visible throughout the program. You can access these variables inside the function as well, unless you have declared a local variable with the same name.

The following code illustrates this. It defines and calls two functions that assign the value to the variable x. The first declares it as local, thereby changing only the local variable. The second one does not declare, so working with x inside the function refers to the global variable x, which was set at the beginning of the example.

 var x = "outside"; var f1 = function() { var x = "inside f1"; }; f1(); console.log(x); // → outside var f2 = function() { x = "inside f2"; }; f2(); console.log(x); // → inside f2 


This behavior helps prevent accidental interactions between functions. If all variables were used anywhere in the program, it would be very difficult to make sure that one variable is not used for different purposes. And if you reused a variable, you would face strange effects when third-party code corrupts the values ​​of your variable. Referring to local variables for functions so that they exist only inside a function, the language makes it possible to work with functions as if with separate small universes, which allows not to worry about the whole code.

Nested scopes


JavaScript distinguishes between not only global and local variables. Functions can be set inside functions, which leads to several levels of locality.

For example, the following rather meaningless function contains two more inside:

 var landscape = function() { var result = ""; var flat = function(size) { for (var count = 0; count < size; count++) result += "_"; }; var mountain = function(size) { result += "/"; for (var count = 0; count < size; count++) result += "'"; result += "\\"; }; flat(3); mountain(4); flat(6); mountain(1); flat(1); return result; }; console.log(landscape()); // → ___/''''\______/'\_ 


The flat and mountain functions see the result variable, because they are inside the function in which it is defined. But they cannot see each other’s count variables, because the variables of one function are outside the scope of another. And the environment outside the landscape function does not see any of the variables defined inside this function.

In short, in every local scope you can see all the areas that contain it. The set of variables available inside the function is determined by the place where this function is described in the program. All variables from the blocks surrounding the function definition are visible — including those defined at the top level in the main program. This approach to scopes is called lexical.

People who have studied other programming languages ​​may think that any block enclosed in braces creates its own local environment. But in JavaScript, only functions create scope. You can use detached units:

 var something = 1; { var something = 2; //  -   something... } //   ... 


But something inside a block is the same variable as the outside. Although such blocks are allowed, it makes sense to use them only for if commands and loops.

If this seems strange to you, it does not seem so only to you. In the JavaScript version 1.7, the let keyword appeared, which works as var, but creates variables that are local to any given block, and not just to a function.

Functions as values


Function names are usually used as the name for a piece of a program. Such a variable is set once and does not change. So it's easy to confuse a function and its name.

But these are two different things. A function call can be used as a simple variable — for example, to use them in any expressions. It is possible to store a function call in a new variable, transfer it as a parameter to another function, and so on. Also, the variable storing the function call remains an ordinary variable and its value can be changed:

 var launchMissiles = function(value) { missileSystem.launch("!"); }; if (safeMode) launchMissiles = function(value) {/*  */}; 


In Chapter 5, we will discuss the wonderful things that can be done by passing function calls to other functions.

Function declaration


There is a shorter version of the expression “var square = function ...”. The function keyword can be used at the beginning of the instruction:

 function square(x) { return x * x; } 


This is a function declaration. The instruction defines the square variable and assigns it the specified function. While everything is ok. There is only one pitfall in this definition.

 console.log("The future says:", future()); function future() { return "We STILL have no flying cars."; } 


This code works, although the function is declared below the code that uses it. This is because function declarations are not part of the normal execution of programs from top to bottom. They "move" to the top of their scope and can be called in any code in this area. Sometimes this is convenient, because you can write the code in the order that looks most meaningful, without worrying about the need to define all the functions above the place where they are used.

And what will happen if we put a function declaration inside a conditional block or a loop? Don't do that. Historically, different platforms for running JavaScript handled such cases in different ways, and the current language standard prohibits doing so. If you want your programs to work consistently, use function declarations only inside other functions or the main program.

 function example() { function a() {} //  if (something) { function b() {} // --! } } 


Call stack

It will be useful to look at how the execution order works with functions. Here is a simple program with several function calls:

 function greet(who) { console.log(", " + who); } greet(""); console.log(""); 


It is processed like this: calling greet makes the passage jump to the beginning of the function. It calls the built-in function console.log, which intercepts the control, does its job and returns control. Then he reaches the end of the greet, and returns to the place where he was called from. The next line again calls console.log.

Schematically, this can be shown as:

 top greet console.log greet top console.log top 


Since the function must return to the place where it was called from, the computer must remember the context from which the function was called. In one case, console.log should go back to greet. In another, she returns to the end of the program.

The place where the computer remembers the context is called the stack. Each time a function is called, the current context is placed at the top of the stack. When the function returns, it takes the top context from the stack and uses it to continue.

Stacking storage requires memory space. When the stack grows too large, the computer stops executing and produces something like “stack overflow” or “too much recursion”. The following code demonstrates this - it asks the computer a very difficult question that leads to endless jumps between two functions. More precisely, it would be endless jumps if the computer had an endless stack. In reality, the stack is overflowing.

 function chicken() { return egg(); } function egg() { return chicken(); } console.log(chicken() + " came first."); // → ?? 


Optional arguments

The following code is completely resolved and runs without problems:

 alert("", " ", " !"); 


Officially, the function takes one argument. However, with such a call she does not complain. She ignores the rest of the arguments and shows “Hello.”

JavaScript is very loyal about the number of arguments passed to a function. If you pass too much, the extra will be ignored. Too little - the value will be set to undefined.

The disadvantage of this approach is that it is possible - and even likely - to pass the wrong number of arguments to the function, and no one will complain to you about it.

The advantage is that you can create functions that take optional arguments. For example, in the next version of the power function, it can be called with both two and one argument — in the latter case, the exponent will be two, and the function works like a square.

 function power(base, exponent) { if (exponent == undefined) exponent = 2; var result = 1; for (var count = 0; count < exponent; count++) result *= base; return result; } console.log(power(4)); // → 16 console.log(power(4, 3)); // → 64 


In the next chapter we will see how in the body of the function you can find out the exact number of arguments passed to it. This is useful because allows you to create a function that takes any number of arguments. For example, console.log uses this property, and displays all the arguments passed to it:

 console.log("R", 2, "D", 2); // → R 2 D 2 


Closures


The ability to use function calls as variables, coupled with the fact that local variables are created anew each time a function is called, brings us to an interesting question. What happens to local variables when the function stops working?

The following example illustrates this question. It declares the wrapValue function, which creates a local variable. It then returns a function that reads this local variable and returns its value.

 function wrapValue(n) { var localVariable = n; return function() { return localVariable; }; } var wrap1 = wrapValue(1); var wrap2 = wrapValue(2); console.log(wrap1()); // → 1 console.log(wrap2()); // → 2 


This is valid and works as it should - access to the variable remains. Moreover, several instances of the same variable can exist at the same time, which once again confirms the fact that local variables are recreated with each function call.

This ability to work with reference to an instance of a local variable is called a closure. The function closing local variables is called closing. It not only frees you from the worries associated with the lifetime of variables, but also allows you to creatively use functions.

With a slight change, we turn our example into a function that multiplies numbers by any given number.

 function multiplier(factor) { return function(number) { return number * factor; }; } var twice = multiplier(2); console.log(twice(5)); // → 10 


A separate variable like localVariable from the wrapValue example is no longer needed. Since the parameter is in itself a local variable.

It will take practice to start thinking in this way. A good version of a mental model is to imagine that a function freezes the code in its body and wraps it in a package. When you see the return function (...) {...}, imagine that this is a control panel with a piece of code, frozen for use later.

In our example, multiplier returns a frozen piece of code, which we store in the variable twice. The last line calls the function contained in the variable, and therefore the stored code is activated (return number * factor;). It still has access to the variable factor, which was determined when the multiplier was called, and it also has access to the argument passed during defrost (5) as a numeric parameter.

Recursion


A function may well call itself if it takes care not to overflow the stack. Such a function is called recursive. Here is an example of an alternative implementation of the exponentiation:

 function power(base, exponent) { if (exponent == 0) return 1; else return base * power(base, exponent - 1); } console.log(power(2, 3)); // → 8 


Something like this, mathematics defines exponentiation, and perhaps it describes the concept more elegantly than a cycle. The function calls itself many times with different arguments to achieve multiple multiplication.

However, such an implementation has a problem - in the usual JavaScript environment it is 10 times slower than the version with the cycle. Passing through the loop is cheaper than calling a function.

The dilemma of “speed versus elegance” is quite interesting. There is a certain gap between convenience for the person and convenience for the car. Any program can be accelerated by making it bigger and more intricate. The programmer is required to find a suitable balance.

In the case of the first exponentiation, the inelegant cycle is fairly simple and straightforward. It does not make sense to replace it with recursion. Often, however, programs work with such complex concepts that I want to reduce efficiency by increasing readability.

The basic rule, which has been repeated more than once, and with which I fully agree - do not worry about speed, until you are sure that the program is slow. If so, find the parts that last the longest and change the elegance to efficiency.

Of course, we should not immediately completely ignore speed. In many cases, as with exponentiation, we do not receive any particular simplicity from elegant solutions. Sometimes an experienced programmer will immediately see that a simple approach will never be fast enough.

I draw attention to this because too many novice programmers cling to efficiency even in small things. The result is more, more difficult and often not without errors. Such programs take longer to write, and they often work not much faster.

But recursion is not always just a less efficient alternative to cycles. Some tasks are easier to solve with recursion. Most often it is a bypass of several branches of a tree, each of which can branch.

Here's a riddle for you: you can get an infinite number of numbers, starting with number 1, and then either adding 5 or multiplying by 3. How do we write a function that, having received a number, tries to find a sequence of such additions and multiplications that lead to a given number? For example, the number 13 can be obtained by first multiplying 1 by 3, and then adding 5 two times. And the number 15 is generally impossible to get.

Recursive solution:

 function findSolution(target) { function find(start, history) { if (start == target) return history; else if (start > target) return null; else return find(start + 5, "(" + history + " + 5)") || find(start * 3, "(" + history + " * 3)"); } return find(1, "1"); } console.log(findSolution(24)); // → (((1 * 3) + 5) * 3) 


This example does not necessarily find the shortest solution — it is satisfied by anyone. I do not expect you to immediately understand how the program works. But let's understand this great exercise in recursive thinking.

The internal find function deals with recursion. It takes two arguments - the current number and the string that contains the record of how we arrived at this number. And returns either a line showing our sequence of steps, or null.

For this function performs one of three actions. If the given number is equal to the goal, then the current story is just the way to achieve it, so it returns. If the given number is greater than the target, there is no sense to continue multiplying and adding, because this will only increase it. And if we have not yet reached the goal, the function tries both possible paths, starting with a given number. She calls herself twice, once with each of the ways. If the first call returns non-null, it is returned. In another case, the second is returned.

To better understand how the function achieves the desired effect, let's review its calls that occur in the search for a solution for the number 13.

 find(1, "1") find(6, "(1 + 5)") find(11, "((1 + 5) + 5)") find(16, "(((1 + 5) + 5) + 5)") too big find(33, "(((1 + 5) + 5) * 3)") too big find(18, "((1 + 5) * 3)") too big find(3, "(1 * 3)") find(8, "((1 * 3) + 5)") find(13, "(((1 * 3) + 5) + 5)") found! 


Indent shows the depth of the call stack. The first time, the find function calls itself twice to check solutions starting with (1 + 5) and (1 * 3).The first call looks for a solution starting with (1 + 5), and with the help of recursion checks all solutions that produce a number less than or equal to the required one. Does not find, and returns null. Then the operator || and proceeds to the function call, which examines the variant (1 * 3). Here we are lucky, because in the third recursive call we get 13. This call returns a string, and each of the || on the way passes this line above, as a result returning the decision.

Grow functions


There are two more or less natural ways of entering functions into a program.

The first is that you write similar code several times. This should be avoided - more code means more room for errors and more reading material for those trying to understand the program. So we take repetitive functionality, select a good name for it and put it into a function.

The second way - you discover the need for some kind of functionality that is worthy of being placed in a separate function. You start with the name of the function, and then write its body. You can even start by writing code that uses the function, before the function itself is defined.

How difficult it is for you to choose a name for a function shows how well you imagine its functionality. Take an example.We need to write a program that displays two numbers, the number of cows and chickens on the farm, followed by the words "cows" and "chickens". To the numbers you need to add zeros to the front so that each takes exactly three positions.

 007  011  


Obviously, we need a function with two arguments. Begin to code.
 //  function printFarmInventory(cows, chickens) { var cowString = String(cows); while (cowString.length < 3) cowString = "0" + cowString; console.log(cowString + " "); var chickenString = String(chickens); while (chickenString.length < 3) chickenString = "0" + chickenString; console.log(chickenString + " "); } printFarmInventory(7, 11); 


If we add to the string .length, we get its length. It turns out that while loops add zeros to the numbers in front, until they get a line of 3 characters.

Done! ( , ), , , ?

, . , , . . :

 //  function printZeroPaddedWithLabel(number, label) { var numberString = String(number); while (numberString.length < 3) numberString = "0" + numberString; console.log(numberString + " " + label); } //  function printFarmInventory(cows, chickens, pigs) { printZeroPaddedWithLabel(cows, ""); printZeroPaddedWithLabel(chickens, ""); printZeroPaddedWithLabel(pigs, ""); } printFarmInventory(7, 11, 3); 


Works! printZeroPaddedWithLabel . – , – . , , :

 //  function zeroPad(number, width) { var string = String(number); while (string.length < width) string = "0" + string; return string; } //  function printFarmInventory(cows, chickens, pigs) { console.log(zeroPad(cows, 3) + " "); console.log(zeroPad(chickens, 3) + " "); console.log(zeroPad(pigs, 3) + " "); } printFarmInventory(7, 16, 3); 


, zeroPad . , . , .

? , , , , , , , ..

– , . . . , , .

Functions and side effects


Functions can be roughly divided into those that are called because of their side effects, and those that are called to get some value. Of course, it is possible to combine these properties in a single function.

The first helper function in the farm example, printZeroPaddedWithLabel, is called because of the side effect: it prints the string. The second, zeroPad, because of the return value. And it is no coincidence that the second function comes in more often than the first. Functions that return values ​​are easier to combine with each other than functions that create side effects.

– , , , – , , - . , , ( ) – . . , . , , , , . , , .

However, do not hesitate to write not entirely clean functions, or to begin the sacred cleaning of the code from such functions. Side effects are often helpful. There is no way to write a clean version of the console.log function, and this feature is quite useful. Some operations are easier to express using side effects.

Total


This chapter showed you how to write your own functions. When the function keyword is used as an expression, returns a pointer to the function call. When it is used as an instruction, you can declare a variable by assigning it a function call.

 //  f     var f = function(a) { console.log(a + 2); }; //   g function g(a, b) { return a * b * 3.5; } 


– . , , , , . , , .

, , . , , , , .

Exercises


Minimum

Math.min, . . min, , .

 console.log(min(0, 10)); // → 0 console.log(min(0, -10)); // → -10 


Recursion

We have seen that the operator% (remainder of division) can be used to determine whether an even number (% 2). And here is another way to determine:

Zero is even.
The unit is odd.
For any number N, the parity is the same as for N-2.

Write the recursive isEven function according to these rules. It must take a number and return a boolean value.

Test it on 50 and 75. Try to ask it -1. Why does she behave this way? Can I somehow fix it?

Test it on 50 and 75. See how it behaves on -1. Why? Can you think of a fix to this?

 console.log(isEven(50)); // → true console.log(isEven(75)); // → false console.log(isEven(-1)); // → ?? 


We count the beans.


N , .charAt(N) ( “”.charAt(5) ) – .length. , ( , “”). 0, , string.length – 1. , 2, 0 1.

countBs, , “B”, .

countChar, countBs, — , ( , “B”). countBs.

Source: https://habr.com/ru/post/240349/


All Articles