Expressive JavaScript: Functions

Original author: Marijn Haverbeke
  • Transfer

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 top of each other, as if composing a wall of small pebbles.

Donald Knuth


You have already seen calls to features such as alert. Functions are the bread and butter of JavaScript programming. 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 repetition, naming subprograms, and isolating subprograms from each other.

The most obvious use of functions is to create a new dictionary. To come up with words for ordinary human prose is 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 dictionary of a programming language is defined more clearly, therefore it is less flexible than human. Therefore, we usually have to add our own words to it in order to avoid unnecessary repetitions.

Function Definition


A function definition is the usual definition of a variable, where the value that the variable receives is a function. For example, the following code defines a square variable that 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 a keyword function. Functions have a set of parameters (in this case, only x), and a body containing instructions that must be followed when calling the function. The body of a function is always enclosed in braces, even if it consists of one 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 instruction returndefines the value returned by the function. When the processing of the program reaches this instruction, it immediately exits the function and returns this value to the place in the code where the function was called from. return without expression returns a value undefined.

Parameters and scope


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

An important property of functions is that variables created inside a function (including parameters) are local inside this function. This means that in the power example, the result variable will be created each time the function is called, and these individual incarnations of it are not connected with 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 also access these variables inside the function, 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 of the variable x. The first declares it as local, thereby changing only the local variable. The second one does not declare, therefore, work with x inside the function refers to the global variable x specified 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 the 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 used the variable repeatedly, you would encounter strange effects when third-party code spoils the values ​​of your variable. By referring to variables local to 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 you not to worry about the whole code.

Nested Scopes


JavaScript distinguishes not only global and local variables. Functions can be defined 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 ​​might think that any block enclosed in braces creates its own local environment. But in JavaScript, only functions create the scope. You can use free-standing blocks:

var something = 1;
{
  var something = 2;
  // Делаем что-либо с переменной something...
}
// Вышли из блока...


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

If this seems strange to you, it seems not only to you. JavaScript version 1.7 introduced the let keyword, which works like var, but creates variables local to any given block, not just a function.

Functions as Values


Function names are usually used as a name for a piece of the program. Such a variable is once set 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, use them in any expressions. It is possible to store a function call in a new variable, pass 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 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 a statement:

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


This is a feature announcement. The instruction defines the variable square and assigns it the specified function. So far, 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.";
}


Such 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 that scope. This is sometimes convenient because you can write code in the order that looks most meaningful, without worrying about having to define all the functions above where they are used.

But what happens if we place a function declaration inside a conditional block or loop? No need to do this. Historically, different platforms for launching JavaScript handled such cases in different ways, and the current language standard prohibits doing so. If you want your programs to run sequentially, 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 take a closer 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 approximately like this: calling greet makes the passage jump to the beginning of the function. It calls the built-in console.log function, which takes control, does its job, and returns control. Then he reaches the end of the greet, and returns to the place from where he was called. The next line calls console.log again.

Schematically, this can be shown as follows:

top
   greet
        console.log
   greet
top
   console.log
top


Since the function must return to the place from where it was called, 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 working.

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

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


Optional arguments

The following code is fully resolved and runs without problems:

alert("Здрасьте", "Добрый вечер", "Всем привет!");


Formally, 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 the function. If you pass too much, the excess will be ignored. Too little - undefined will be set to absent.

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

The plus 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 either two or one argument - in the latter case, the exponent will be equal to 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 function body 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


Short circuits


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

The following example illustrates this question. It declares a wrapValue function that creates a local variable. Then it 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, at the same time, several instances of the same variable may exist, which once again confirms the fact that local variables are recreated with each function call.

This ability to work with reference to some instance of a local variable is called a closure. A function that closes local variables is called a closure. It not only frees you from worries related to the lifetime of variables, but also allows you to use functions creatively.

With a little 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 a local variable in itself.

It will take practice to start thinking in this way. A good version of the 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 it is a remote control for a piece of code frozen for use later.

In our example, multiplier returns a frozen piece of code that we store in the variable twice. The last line calls the function enclosed in the variable, in connection with which the saved code is activated (return number * factor;). He still has access to the factor variable, which was determined when the multiplier was called, and he 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 exponentiation implementation:

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


Something like that mathematicians define exponentiation, and perhaps this describes the concept more elegantly than a cycle. A function calls itself many times with different arguments to achieve multiple multiplication.

However, this implementation has a problem - in a normal JavaScript environment, it is 10 times slower than the version with a loop. Loop through is cheaper than calling a function.

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

In the case of the first exponentiation, the inelegant cycle is quite simple and understandable. It makes no sense to replace it with recursion. Often, however, programs work with such complex concepts that you want to reduce efficiency by increasing readability.

The main rule, which has been repeated more than once, and with which I completely 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 elegance to efficiency.

Of course, we should not immediately completely ignore the performance. In many cases, as with exponentiation, we do not get much simplicity from elegant solutions. Sometimes an experienced programmer immediately sees that a simple approach will never be fast enough.

I draw attention to this because too many novice programmers grab onto efficiency even in the smallest detail. The result is bigger, more complex and often not without errors. Such programs take longer to write, and they often do not work much faster.

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

Here's the riddle for you: you can get an infinite number of numbers, starting with the number 1, and then either adding 5 or multiplying by 3. How can we write a function that, having received a number, tries to find a sequence of 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 cannot be obtained like that.

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 recursive thinking exercise.

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

To do this, the function performs one of three actions. If the given number is equal to the goal, then the current history is precisely the way to achieve it, therefore it returns. If the given number is larger than the goal, there is no point in continuing to multiply and add, because this way it will only increase. 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 returns. Otherwise, the second is returned.

To better understand how a function achieves the desired effect, let's look at its calls that occur in search of 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!


Indentation shows the depth of the call stack. For the first time, the find function calls itself twice to check for solutions starting with (1 + 5) and (1 * 3). The first call searches 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 explores the option (1 * 3). Here luck awaits us, because in the third recursive call we get 13. This call returns a string, and each of the operators || along the way it passes this line above, as a result, returning a solution.

Growing Functions


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

First, 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 in the function.

The second way - you discover the need for some new 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 a function before the function itself is defined.

How difficult it is for you to find 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 hens on the farm, followed by the words “cows” and “hens”. To the numbers you need to add zeros in front so that each occupies exactly three positions.

007 Коров
011 Куриц


Obviously, we need a function with two arguments. We 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 .length to the string, we get its length. It turns out that while loops add zeros in front of the numbers until they get a 3-character string.

Done! But as soon as we were about to send the code to the farmer (along with a fair check, of course), he calls and tells us that he has pigs on his farm, and could we add the output of the number of pigs to the program?

Of course it is possible. But when we start to copy and paste the code from these four lines, we understand that we need to stop and think. There must be a better way. Trying to improve the program:

// выводСДобавлениемНулейИМеткой
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! But the name printZeroPaddedWithLabel is a bit strange. It combines three things - output, adding zeros and label - in one function. Instead of inserting the entire repeating fragment into the function, let's single out one concept:

// добавитьНулей
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);


A function with a good, clear name zeroPad makes code easier to understand. And it can be used in many situations, not only in our case. For example, to display formatted tables with numbers.

How smart and versatile should functions be? We can write both the simplest function, which complements the number with zeros to three positions, and a heaped-up general purpose function for formatting numbers that supports fractions, negative numbers, dot alignment, padding with different characters, etc.

A good rule of thumb is to add only the functionality that is most useful to you. Sometimes you are tempted to create general-purpose frameworks for every small need. Resist him. You will never finish the job, but simply write a bunch of code that no one will use.

Functions and side effects


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

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

A pure function is a special kind of function that returns a value that not only has no side effects, but also does not depend on the side effects of the rest of the code - for example, it does not work with global variables that can be accidentally changed elsewhere. A pure function, when called with the same arguments, returns the same result (and does nothing else) - which is pretty nice. It’s easy to work with her. A call to such a function can be mentally replaced by the result of its operation, without changing the meaning of the code. When you want to test such a function, you can simply call it, and be sure that if it works in this context, it will work in any one. Not so pure functions can return different results depending on many factors, and have side effects that are difficult to verify and consider.

However, do not be shy to write not entirely pure functions, or start a sacred purification 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 function 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 a 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;
}


The key to understanding functions is local scope. The parameters and variables declared inside the function are local to it, are recreated every time it is called, and are not visible from the outside. Functions declared inside another function have access to its scope.

It is very useful to separate the different tasks performed by the program into functions. You don’t have to repeat, functions make the code more readable, dividing it into semantic parts, just as the chapters and sections of the book help in organizing plain text.

Exercises


Minimum

In the previous chapter, the Math.min function was mentioned, which returns the smallest of the arguments. Now we can write such a function ourselves. Write a min function that takes two arguments and returns the minimum of them.

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 if an even number (% 2). And here is another way of determining:

Even zero.
The unit is odd.
Any number N has the same parity as N-2.

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

Test it at 50 and 75. Try setting it to -1. Why does she behave this way? Is there any way to fix it?

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

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


Count the beans.


The character number N of the line can be obtained by adding .charAt (N) to it (“line” .charAt (5)) - similarly with getting the length of the line using .length. The return value will be a string consisting of one character (for example, “k”). The first character of the string has position 0, which means that the last character will have string.length - 1. In other words, the two-character string has length 2, and the positions of its characters will be 0 and 1.

Write a countBs function that takes a string to as an argument, and returns the number of characters “B” contained in the string.

Then write a function countChar, which works like countBs, only accepts the second parameter - the character that we will look for in the string (instead of just counting the number of characters “B”). To do this, redo the countBs function.

Also popular now: