Programming/JavaScript

[Eloquent JS] Ch3. Functions

dododoo 2020. 2. 17. 12:52

The concept of wrapping a piece of program in a value has many uses.

Defining a function

  • A function definition is a regular binding where the value of the binding is a function.

  • const square = function(x) {
        return x * x;
    };
    
    console.log(square(12));
  • A function is created with an expression that starts with the keyword function.

  • const makeNoise = function() {
        console.log("Pling!");
    };
    
    const power = function(base, exponent) {
        let result = 1;
          for (let count = 0; count < exponent; count++) {
            result *= base;
        }
          return result;
    };
  • Some functions produce a value, such as power and square, and some don’t, such as makeNoise, whose only result is a side effect. A return statement determines the value the function returns.

  • A return keyword without an expression after it will cause the function to return undefined.

  • Functions that don’t have a return statement at all, such as makeNoise, similarly return undefined.

Bindings and scopes

  • Bindings declared with let and const are in fact local to the block that they are declared in, so if you create one of those inside of a loop, the code before and after the loop cannot “see” it.
  • In pre-2015 JavaScript, only functions created new scopes, so old-style bindings, created with the var keyword, are visible throughout the whole function that they appear in—or throughout the global scope, if they are not in a function.
  • let x = 10;
    if (true) {
        let y = 20;
        var z = 30;
        console.log(x + y + z);
        // → 60
    }
    // y is not visible here
    console.log(x + z);
    // → 40

Nested scope

  • JavaScript distinguishes not just global and local bindings. Blocks and functions can be created inside other blocks and functions, producing multiple degrees of locality.
  • const hummus = function(factor) {
        const ingredient = function(amount, unit, name) {
            let ingredientAmount = amount * factor;
            if (ingredientAmount > 1) {
                unit += "s";
            }
            console.log(`${ingredientAmount} ${unit} ${name}`);
        };
        ingredient(1, "can", "chickpeas");
        ingredient(0.25, "cup", "tahini");
        ingredient(0.25, "cup", "lemon juice");
        ingredient(1, "clove", "garlic");
        ingredient(2, "tablespoon", "olive oil");
        ingredient(0.5, "teaspoon", "cumin");
    };
  • The set of bindings visible inside a block is determined by the place of that block in the program text. Each local scope can also see all the local scopes that contain it, and all scopes can see the global scope.
  • This approach to binding visibility is called lexical scoping.

Functions as values

  • A function binding usually simply acts as a name for a specific piece of the program. Such a binding is defined once and never changed.
  • This makes it easy to confuse the function and its name.
  • But the two are different. A function value can do all the things that other values can do—you can use it in arbitrary expressions, not just call it.
  • It is possible to store a function value in a new binding, pass it as an argument to a function, and so on.
  • Similarly, a binding that holds a function is still just a regular binding and can, if not constant, be assigned a new value, like so:
  • let launchMissiles = function() {
        missileSystem.launch("now");
    };
    if (safeMode) {
        launchMissiles = function() {/* do nothing */};
    }

Declaration notation

  • There is a slightly shorter way to create a function binding. When the function keyword is used at the start of a statement, it works differently.

  • function square(x) {
        return x * x;
    }
  • This is a function declaration. The statement defines the binding square and points it at the given function. It is slightly easier to write and doesn’t require a semicolon after the function.

  • There is one subtlety with this form of function definition.

  • console.log("The future says:", future());
    
    function future() {
        return "You'll never have flying cars";
    }
  • The preceding code works, even though the function is defined below the code that uses it. Function declarations are not part of the regular top-to-bottom flow of control. They are conceptually moved to the top of their scope and can be used by all the code in that scope.

  • This is sometimes useful because it offers the freedom to order code in a way that seems meaningful, without worrying about having to define all functions before they are used.

Arrow functions

  • There’s a third notation for functions, which looks very different from the others.
  • const power = (base, exponent) => {
        let result = 1;
        for (let count = 0; count < exponent; count++) {
            result *= base;
        }
        return result;
    };
  • The arrow comes after the list of parameters and is followed by the function’s body. It expresses something like “this input (the parameters) produces this result (the body)”.
  • When there is only one parameter name, you can omit the parentheses around the parameter list. If the body is a single expression, rather than a block in braces, that expression will be returned from the function. So, these two definitions of square do the same thing:
  • const square1 = (x) => { return x * x; };
    const square2 = x => x * x;
  • When an arrow function has no parameters at all, its parameter list is just an empty set of parentheses.
  • const horn = () => { console.log("Toot"); };
  • There’s no deep reason to have both arrow functions and function expressions in the language. Apart from a minor detail, which we’ll discuss in Chapter 6, they do the same thing.
  • Arrow functions were added in 2015, mostly to make it possible to write small function expressions in a less verbose way.

Optional Arguments

  • The following code is allowed and executes without any problem:

  • function square(x) { return x * x; }
    console.log(square(4, true, "hedgehog"));
    // → 16
  • We defined square with only one parameter. Yet when we call it with three, the language doesn’t complain. It ignores the extra arguments and computes the square of the first one.

  • JavaScript is extremely broad-minded about the number of arguments you pass to a function. If you pass too many, the extra ones are ignored. If you pass too few, the missing parameters get assigned the value undefined.

  • The downside of this is that it is possible—likely, even—that you’ll accidentally pass the wrong number of arguments to functions. And no one will tell you about it.

  • The upside is that this behavior can be used to allow a function to be called with different numbers of arguments. For example, this minus function tries to imitate the - operator by acting on either one or two arguments:

  • function minus(a, b) {
        if (b === undefined) return -a; // Note: ===
        else return a - b;
    }
    
    console.log(minus(10));
    // → -10
    console.log(minus(10, 5));
    // → 5
  • If you write an = operator after a parameter, followed by an expression, the value of that expression will replace the argument when it is not given.

  • For example, this version of power makes its second argument optional. If you don’t provide it or pass the value undefined, it will default to two, and the function will behave like square.

  • function power(base, exponent = 2) {
        let result = 1;
        for (let count = 0; count < exponent; count++) {
            result *= base;
        }
        return result;
    }
    
    console.log(power(4));
    // → 16
    console.log(power(2, 6));
    // → 64
  • We will see a way in which a function body can get at the whole list of arguments it was passed. This is helpful because it makes it possible for a function to accept any number of arguments. (Ex. console.log)

Closure

  • The ability to treat functions as values, combined with the fact that local bindings are re-created every time a function is called, brings up an interesting question. What happens to local bindings when the function call that created them is no longer active?

  • The following code shows an example of this. It defines a function, wrapValue, that creates a local binding. It then returns a function that accesses and returns this local binding.

  • function wrapValue(n) {
        let local = n;
        return () => local;
    }
    
    let wrap1 = wrapValue(1);
    let wrap2 = wrapValue(2);
    console.log(wrap1());
    // → 1
    console.log(wrap2());
    // → 2
  • Both instances of the binding can still be accessed. This situation is a good demonstration of the fact that local bindings are created anew for every call, and different calls can’t trample on one another’s local bindings.

  • This feature—being able to reference a specific instance of a local binding in an enclosing scope—is called closure.

  • A function that references bindings from local scopes around it is called a closure.

  • This behavior not only frees you from having to worry about lifetimes of bindings but also makes it possible to use function values in some creative ways.

  • function multipliers(factor) {
        return number => number * factor;
    }
    
    let twice = multiplier(2);
    console.log(twice(5));
    // → 10
  • The explicit local binding from the wrapValue example isn’t really needed since a parameter is itself a local binding.

  • Thinking about programs like this takes some practice. A good mental model is to think of function values as containing both the code in their body and the environment in which they are created.

  • When called, the function body sees the environment in which it was created, not the environment in which it is called.

Recursion

  • But this implementation has one problem: in typical JavaScript implementations, it’s about three times slower than the looping version. Running through a simple loop is generally cheaper than calling a function multiple times.

  • The dilemma of speed versus elegance is an interesting one. The programmer has to decide on an appropriate balance.

  • Often, though, a program deals with such complex concepts that giving up some efficiency in order to make the program more straightforward is helpful.

  • Worrying about efficiency can be a distraction. It’s yet another factor that complicates program design, and when you’re doing something that’s already difficult, that extra thing to worry about can be paralyzing.

  • Therefore, always start by writing something that’s correct and easy to understand. If you’re worried that it’s too slow—which it usually isn’t since most code simply isn’t executed often enough to take any significant amount of time—you can measure afterward and improve it if necessary.

  • Recursion is not always just an inefficient alternative to looping. Some problems really are easier to solve with recursion than with loops. Most often these are problems that require exploring or processing several “branches”, each of which might branch out again into even more branches.

  • function findSolution(target) {
        function find(current, history) {
            if (current == target) {
                return history;
            } else if (current > target) {
                return null;
            } else {
                return find(current + 5, `(${history} + 5)`) ||
                       find(current * 3, `(${history} * 3)`);
            }
        }
    
        return find(1, "1");
    }
    
    console.log(findSolution(24));
  • If the first call returns something that is not null, it is returned. Otherwise, the second call is returned, regardless of whether it produces a string or null.

Growing functions

  • There are two more or less natural ways for functions to be introduced into programs.

  • The first is that you find yourself writing similar code multiple times. You’d prefer not to do that. Having more code means more space for mistakes to hide and more material to read for people trying to understand the program. So you take the repeated functionality, find a good name for it, and put it into a function.

  • The second way is that you find you need some functionality that you haven’t written yet and that sounds like it deserves its own function. You’ll start by naming the function, and then you’ll write its body. You might even start writing code that uses the function before you actually define the function itself.

  • How difficult it is to find a good name for a function is a good indication of how clear a concept it is that you’re trying to wrap.

  • function printFarmInventory(cows, chickens) {
        let cowString = String(cows);
        while (cowString.length < 3) {
            cowString = "0" + cowString;
        }
        console.log(`${cowString} Cows`);
        let chickenString = String(chickens);
        while (chickenString.length < 3) {
            chickenString = "0" + chickenString;
        }
        console.log(`${chickenString} Chickens`);
    }
    printFarmInventory(7, 11);
  • (Repeated functionality)

  • function printZeroPaddedWithLabel(number, label) {
        let numberString = String(number);
        while (numberString.length < 3) {
            numberString = "0" + numberString;
        }
        console.log(`${numberString} ${label}`);
    }
    
    function printFarmInventory(cows, chickens, pigs) {
        printZeroPaddedWithLabel(cows, "Cows");
        printZeroPaddedWithLabel(chickens, "Chickens");
        printZeroPaddedWithLabel(pigs, "Pigs");
    }
    
    printFarmInventory(7, 11, 3);
  • printZeroPaddedWithLabel conflates three things—printing, zero-padding, and adding a label—into a single function.

  • Let’s try to pick out a single concept.

  • function zeroPad(number, width) {
        let string = String(number);
        while (string.length < width) {
            string = "0" + string;
        }
        return string;
    }
    
    function printFarmInventory(cows, chickens, pigs) {
        console.log(`${zeroPad(cows, 3)} Cows`);
        console.log(`${zeroPad(chickens, 3)} Chickens`);
        console.log(`${zeroPad(pigs, 3)} Pigs`);
    }
    
    printFarmInventory(7, 16, 3);
  • A function with a nice, obvious name like zeroPad makes it easier for someone who reads the code to figure out what it does. And such a function is useful in more situations than just this specific program.

  • How smart and versatile should our function be?

  • A useful principle is to not add cleverness unless you are absolutely sure you’re going to need it. It can be tempting to write general “frameworks” for every bit of functionality you come across.

  • Resist that urge. You won’t get any real work done—you’ll just be writing code that you never use.

Functions and side effects

  • Functions can be roughly divided into those that are called for their side effects and those that are called for their return value. (Though it is definitely also possible to both have side effects and return a value.)
  • printZeroPaddedWithLabel is called for its side effect: it prints a line. zeroPad is called for its return value.
  • It is no coincidence that the second is useful in more situations than the first. Functions that create values are easier to combine in new ways than functions that directly perform side effects.
  • A pure function is a specific kind of value-producing function that not only has no side effects but also doesn’t rely on side effects from other code—for example, it doesn’t read global bindings whose value might change.
  • A pure function has the pleasant property that, when called with the same arguments, it always produces the same value (and doesn’t do anything else).
  • Still, there’s no need to feel bad when writing functions that are not pure or to wage a holy war to purge them from your code. Side effects are often useful.

Exercises

Minimum

function min(a, b) {
    return (a < b)? a : b;
}

Recursion

// ver 1.0
function isEven(num) {
    if (num == 0) return true;
    else if (num == 1) return false;
    else return isEven(num - 2); 
}
// ver 2.0
function isEven2(num) {
    if (num == 0) return true;
    else if (num == 1) return false;
    else return (num < 0)? isEven(num + 2) : isEven(num - 2); 
}

Bean counting

const countChar = (string, char) => {
    let count = 0;
    for (let i = 0; i != string.length; ++i) {
        if (string[i] === char) ++count;
    }
    return count;
};

const countBs = string => countChar(string, "B");

'Programming > JavaScript' 카테고리의 다른 글

[EloquentJS] Ch7. Project: A Robot  (0) 2020.04.14
[EloquentJS] Ch6. The Secret Life of Objects  (0) 2020.04.14
[EloquentJS] Ch5. Higher-order Functions  (0) 2020.04.14
new Function() vs. Function()  (0) 2020.03.12
eval() with let  (0) 2020.03.12