Javascript: functions

·

10 min read

functions are one of the most important fundamental building blocks of programs almost in all programming languages. A function is a block of code that is defined once and invoked any number of times. In Javascript the typeof () operator for a function returns the string "function" but they can also behave as an object. They can have properties like an object. Functions are parameterized. These parameters act as local variables for the body of the function. Functions return a value explicitly or implicitly. If no return value is mentioned by the user then by default it returns undefined. Let's look at how functions are declared.

Defining function

  1. Function declaration

    The simplest way to define a function is a function declaration which includes

    a function keyword, identifier, parenthesis around comma-separated parameters

    and pair of enclosing curly brackets. One of the important things to understand about the function declarations is they are hoisted to the top of the script, function, or block which means they can be invoked before they are defined.

     function add(x,y){
           return x+y
     }
     add(4,5)
    
  2. Function expression

    Another way of defining a function is to include a function expression. It is similar to function declaration but they appear in the context of larger expressions. The syntax is similar to function declaration but the identifier is optional. so it is up to you to assign the function to a variable to use it later in the program. Unlike function declarations, they are not hoisted so you cannot invoke them before they are defined.

     let square = function (x) {return x*x}
     square(5)
    
  3. Arrow functions

    arrow function was introduced in ES6 which is a very compact syntax. It does not use function keyword and function name. It adds an arrow '=>' between parenthesis and body. It is similar to function expression as it needs to be assigned to a constant or variable. Arrow functions are useful when you want to pass a function as an argument to another function.

     const sum = (x, y) => { return x + y; }
    

    If the body of the arrow function is a single return statement, you can omit the return keyword and the curly braces. Also if there is only one parameter you can omit the parenthesis as well.

     const sum = (x, y) => x + y // single return statement
     const square = x => x*x // single parameter
    

    Arrow functions differ from regular function declarations in the following ways

    1. They are not hoisted

    2. They inherit the value of this keyword from the environment they are defined and have a different value of this.

    3. They also do not have prototype property and cannot be used as constructor functions.

Invoking functions

functions can be invoked in the following ways.

  1. as regular functions

    function expression that evaluates to function object followed by an open parenthesis, comma-separated list of zero or more arguments, and closed parenthesis. In a regular function the value of this in strict mode is "undefined" and in non-strict mode is a global object.

     function multiply(x,y){
     console.log(this)
     return x*y
     }
    
     multiply(5,6)  // => "undefined" in strict mode, global object in non-strict mode, return value 30
    
  2. as methods

    when a function expression is invoked as an object property then it is known as a method invocation. In method invocation the invocation context becomes the object itself which means the value of this inside the function will be the object.

    If there is any nested function it is invoked just like a regular function. They do not inherit the value of this from the outer function so the value of this is the same as the regular function.

    Array functions behave differently compared to function expressions. They inherit the value of this from the outer function in which they are defined. So if there is an arrow function inside a method then the value of this inside the arrow function is the same the value of this inside its enclosing function.

     let o = {x:3,y:4,add: function (x,y){return this.x+this.y}}
     o.add() // =>7
     let a = {
         x:1,
         y:2,
         add: function (){ 
                  console.log(this) 
                  let functionExpression =  function (){ 
                      console.log(this) 
                  }
                  let  arrowFunction = () =>{ 
                      console.log(this) 
                  }
                  functionExpression()
                  arrowFunction()
     }
     }
    
     a.add() 
     // object a
     // global object(non-strict mode) or undefined (strict mode)
     // object a
    
  3. as constructors

    when invoked using the keyword new it is known as constructor invocation. It creates a new object , initializes it, and inherits properties from the prototype property of the constructor.

     let array = new Array() // empty array, inherits from Array.prototype
     let obj = new Object()  // empty object inhertis from Object.protoype
    
     function student (name ,age){
           this.name = name
           this.age = age
     }
     let newStudent = new student("jamie fox",18)
     newStudent.name // => jamie fox
    
  4. using call(),apply()

    call() and apply() are methods of functions as we know functions can behave like objects. You can invoke any function other than the arrow function as a method of any object even if it is not defined in that object.

     let o = {}
     function abc(){
         this.x = 1; 
         this.y = 2;
     }
    
     abc.call(o) 
     o.x // 1
     o.y // 2
    

Function parameters and arguments

when you invoke a function with fewer arguments than the defined no of parameters the rest of the parameters are evaluated to default value, which is undefined. In ES6 and later you can give a default value explicitly.

function add(x,z,y=5){
console.log(z)
return x+y;
}
add(3) // undefined, 8

when you invoke a function with arguments more than the defined no of parameters, the extra arguments are ignored. If you want to collect those arguments you can use the rest parameter which is three dots followed by an identifier. It collects all the extra arguments into an array. It was introduced in ES6.

function max(first , ...rest){
    let max = first;
    for(let i of rest){
        if(i> max){
            max = i
        }
    }
    return max;
}
max(1, 10, 100, 2, 3, 1000, 4, 5, 6) // => 1000

The spread operator is similar to the rest parameter as both have the same syntax. If it is used in function invocation it is used as a spread operator which spreads out the elements or properties. If it is used in a function parameter it is used as a rest parameter which collects all the extra arguments into an array.

let numbers = [5, 2, 10, -1, 9, 100, 1];
Math.min(...numbers) // => -1

First class functions

javascript functions are first-class functions which means they can be treated as values. This way you can return a function, pass a function as an argument, assign a function to a variable or object property and so on. This is a powerful feature.

function square(x) { return x*x; }
let s = square; // Now s refers to the same function that square does
square(4) // => 16
s(4) // => 16

let o = {square: function(x) { return x*x; }}; // An object literal with a function as property
let y = o.square(16); // y == 256 method invocation

Closures

javascript uses lexical scoping which means that functions are executed using the scope that was in effect when they are defined and not the scope that was in effect when they are invoked. This is a very powerful feature to have. so the functions when they are defined will have a reference to the scope that appears at that time. This combination of functions and their reference to the lexical scope is known as closure. Look at the following example.

let scope = "global scope"
function checkScope(){
    let scope = "local scope"
    function f(){
         return scope
     }
    return f()
}
checkScope() // => "local scope"
let scope = "global scope"; // A global variable
function checkscope() {
    let scope = "local scope"; // A local variable
    function f() { return scope; } 
    return f;
}
let s = checkscope()(); //=> "local scope"

In the above examples the first one we invoked the nested function and returned the value of the nested function. This is a pretty straightforward answer. But in the second one, we returned the nested function. It was invoked outside of the place where it was defined. But still, the answer is the same because it captures or has the binding of the variables from the scope where it was defined.

Let's see another example where we generate a counter using a closure. The use of this method is the count variable is private and only accessible to the inner function. This can avoid bugs where it can be reset by other programs.

function counter(){
    let count = 0
    return function (){return ++count}
}
let count = counter()
count() // => 1
count() // => 2
count() // => 3

The below example is an interesting problem.

function f(){
    let functionsArray = []
    for(var i=0; i<10; i++){
        functionsArray[i] = function (){return i}
    }
    return functionsArray;
}
let returnedArray = f()
returnedArray[0]() // => 10
returnedArray[3]() // => 10
returnedArray[8]() // => 10

The above example creates 10 closures where they share the variable i. Every function value in the returned array is 10 because of the 'var'. Unlike let var is not block scoped. It is available throughout the function. So the same i is getting incremented and when the f() returns the value of i is 10. And they share this i. This is why every function returnedArray returns 10;

The solution to this is to use let instead of var which is block scoped. After every iteration brand new i is created and bound to the nested function. So they return different values;

function f(){
    let functionsArray = []
    for(let i=0; i<10; i++){
        functionsArray[i] = function (){return i}
    }
    return functionsArray;
}
let returnedArray = f()
returnedArray[0]() // => 0
returnedArray[3]() // => 3
returnedArray[8]() // => 8

Function methods and properties

  1. call() and apply()

    These two methods allow you to invoke a function as if it was a method of an arbitrary object. The first argument to both these methods is an object. This object is the invocation context and becomes the value of the "this" keyword inside the body of the function. All the arguments after the first argument to call() are the values that are passed to the function that was invoked on. similarly, apply() accepts arguments after the first argument but in the form of an array. These array elements are passed to the function that was invoked on.

     function add(a,b){
     return this.x + this.y + a + b;
     }
     let o = {x:1,y:2}
     add.call(o,4,5)     // => 12
     add.apply(o,[4,5]) // => 12
    
  2. bind()

    The purpose of bind() is to attach a function to an object. When you use bind on a function f and pass an object it returns a new function. Invoking this new function invokes the original function f as a method of the object. Any arguments passed to the new function are passed to the original function.

     function f(y) { return this.x + y; } // This function needs to be bound
     let o = { x: 1 }; // An object we'll bind to
     let g = f.bind(o); // Calling g(x) invokes f() on o
     g(2) // => 3
     let p = { x: 10, g }; // Invoke g() as a method of this object
     p.g(2) // => 3: g is still bound to o, not p.
    
  3. length property

    The length property of a function specifies the number of parameters that were defined when the function was first created. This is also known as arity.

    If the rest parameter is defined then it is not included in the length.

     function sum(a,b){
         return a+b
     }
    
     sum.length() //=> 2
    
     function product(...rest){
         let product = 1;
         for(let i of rest){
             product *= i
         }
     return product
     }
     product.length // => 0
    
  4. name property

    The name property specifies the name of the function that was mentioned when it was created or the name of the variable, property, or constant to which an anonymous function expression is assigned.

     function greeting(name){
         retuurn `hello ${name}`
     }
     greeting.name // => greeting
     let square = function (x) { return x*x}
     square.name // square
    

prototype property

All functions except arrow functions have a prototype property. when an object is created using a constructor function it inherits all the properties that are mentioned in the prototype property of that constructor function. This will be explained in detail in the class chapter.

function student(name,age){
    this.name = name
    this.age = age
}
student.prototype.greeting = function () {return `hello ${this.name}`}
let s = new student("joe",25)
s.greeting() // "hello joe"

Source:

  1. JavaScript: The Definitive Guide, 7th Edition by David Flanagan