JS 𝑓unctions

by Kiril Knysh

function as a function


function add(left, right) {
  return left + right;
}
            

function as a method


const obj = {
  left: 10,
  add: function(right) {
    this.left += right;

    return this.left;
  },
};
            

function as a constructor


function Pie(radius, height) {
  this.radius = radius;
  this.height = height;
}

const pie = new Pie(20, 5);
            

function is an object (first-class function)


function logPrice(price) {
  console.log(price);
}

function currencize(logger, currency) {
  function currencized(price) {
    return logger(currency + price);
  }

  currencized.currency = currency;

  return currencized;
}

const logPriceEuro = currencize(logPrice, '€');

logPriceEuro(42);
            

define function:

  • function declaration
  • function expression

function declaration


function functionName([parameters]) {
  // function body
}
            

add(1, 1);

function add(left, right) {
  return left + right;
}
            

function declaration


console.log(pie);

var pie = 'lemon'; // can be fixed by var -> let
console.log(pie);

pie = 'choco';
function pie() {}
console.log(pie);
            

function expression


variableName = function([parameters]) {
  // function body
};
            

const add = function(left, right) {
  return left + right;
}

add(1, 1);
            

named function expression


variableName = function functionName([parameters]) {
  // function body
};
            

const getFibonacci = function fibonacci(n) {
  if (n <= 1) {
    return 1;
  }

  return fibonacci(n - 1) + fibonacci(n - 2);
}

getFibonacci(5);
fibonacci(10); // ReferenceError: fibonacci is not defined
            

Function constructor


const add = new Function('left', 'right', 'return left + right;');

add(1, 41);
            

Every JavaScript function is actually a Function object.

passing arguments to function

  • primitives as passed "by value"
  • objects as passed "by reference"

passing primitives to function


function cookPie(type, radius, slice) {
  console.log('#cookPie:0', type, radius, slice);

  type = 'choco';
  radius = 42;
  slice = false;

  console.log('#cookPie:1', type, radius, slice);
}

const type = 'lemon';
const radius = 30;
const slice = true;

cookPie(type, radius, slice);
console.log('#global:0', type, radius, slice);
            

passing objects to function


function cookPie(pieOptions) {
  console.log('#cookPie:0', pieOptions);

  pieOptions.type = 'choco';
  console.log('#cookPie:1', pieOptions);

  pieOptions = {};
  console.log('#cookPie:2', pieOptions);
}

const options = { type: 'lemon', radius: 30, slice: true };

cookPie(options);
console.log('#global:0', options);
            

passing arguments to function

  • extra arguments are ignored

    
    function cookPie(type, radius, slice) {
      console.log('#cookPie:0', type, radius, slice);
    }
    cookPie('lemon', 21, true, 'extra', 'args');
                    
  • missing arguments set to undefined

    
    function cookPie(type, radius, slice) {
      console.log('#cookPie:0', type, radius, slice);
    }
    cookPie('lemon');
                    

passing arguments to function


function cookPie() {
  console.log('#cookPie:0', arguments[0], arguments[1], arguments[2]);
  console.log('#cookPie:1', arguments.length);
  console.log('#cookPie:2', arguments.callee);
  console.log('#cookPie:3', Array.isArray(arguments));
}

cookPie('lemon', 14, true);
            

passing ...arguments to function


function cookPie(...args) {
  console.log('#cookPie:0', args[0], args[1], args[2]);
  console.log('#cookPie:1', args.length);
  console.log('#cookPie:2', Array.isArray(args));
}

cookPie('lemon', 14, true);
            

function sharePie(type, ...friends) {
  console.log('#cookPie:0', type, friends);
}

sharePie('lemon', 'Albert', 'Alan', 'Stephen');
            

returning value from function


return value;
            

return; // return undefined;
            

except constructors whose default return value is this

returning value from function


function returnPieType(n) {
  if (n === 0) {
    return 'lemon';
  }
  if (n === 1) {
    return;
  }
  if (n === 2) {}
  if (n === 3) {
    this.type = 'choco';
  }
}

console.log(returnPieType(0));
console.log(returnPieType(1));
console.log(returnPieType(2));
console.log(new returnPieType(3));
            

function as data


function cookPie(type, radius, slice) {
  console.log('#cookPie:0', type, radius, slice);
}

const pieFlow = [mixIngredients, cookPie];
pieFlow[1]('lemon', 42, false);

cookPie.oven = 'oven#1';

const lemonPie = {
  type: 'lemon',
  cook: cookPie,
};
            

function as data


function callCounter(func) {
  func.callCounter = 0;

  return function(...args) {
    func.callCounter += 1;

    return func(...args);
  };
}

function cookPie(type, radius, slice) {
  console.log('#cookPie:0', type, radius, slice);
}

const countedCookPie = callCounter(cookPie);
            

this

this

  • determined in runtime
  • immutable

this in global context


function cookPie() {
  console.log(this);
}

cookPie();
            

value of this is determined by the invocation form, not context of definition


function cookPie() {
  console.log(this.type);
}

const lemonPie = { type: 'lemon' };
const chocoPie = { type: 'choco' };

lemonPie.cookPie = cookPie;
chocoPie.cookPie = cookPie;

lemonPie.cookPie(); // 'lemon'
chocoPie.cookPie(); // 'choco'

cookPie(); // undefined

new cookPie(); // undefined
            

value of this


function cookPie() {
  console.log(this); // window
}

function cookStrictPie() {
  'use strict';
  console.log(this); // undefined
}
            

const lemonPie = {
  type: 'lemon',
  cookPie: function () { console.log(this); },
};

lemonPie.cookPie();

const cookPie = lemonPie.cookPie;

cookPie();
            

value of this


function Pie(type, radius) {
  this.type = type;
  this.radius = radius;
  this.cook = function () {
    console.log(this);
  };
}

const lemonPie = new Pie('lemon', 38);
lemonPie.cook();

const chocoPie = Pie('choco', 24);
chocoPie.cook();
            

value of deep this


function Pie(type, radius) {
  this.type = type;
  this.radius = radius;
  this.cook = function () {
    const that = this;
    console.log(that); // pie

    const innerCook = function() {
      console.log(this); // window
      console.log(that); // pie
    };

    innerCook();
  };
}

const lemonPie = new Pie('lemon', 38);
lemonPie.cook();
            

change function's context:

  • call
  • apply
  • bind

change function's context: call


function cookPie() {
  console.log('#cookPie', this.type, this.radius);
}

const lemonPie = { type: 'lemon', radius: 9, cook: cookPie };
lemonPie.cook();

const chocoPie = { type: 'choco', radius: 17 };
lemonPie.cook.call(chocoPie);
            

function cookPie(radius, slice) {
  console.log('#cookPie', this.type, radius, slice);
}

const lemonPie = { type: 'lemon', radius: 9 };
cookPie.call(lemonPie, 14, false);
            

change function's context: apply


function cookPie() {
  console.log('#cookPie', this.type, this.radius);
}

const lemonPie = { type: 'lemon', radius: 9, cook: cookPie };
lemonPie.cook();

const chocoPie = { type: 'choco', radius: 17 };
lemonPie.cook.apply(chocoPie);
            

function cookPie(radius, slice) {
  console.log('#cookPie', this.type, radius, slice);
}

const lemonPie = { type: 'lemon', radius: 9 };
cookPie.apply(lemonPie, [14, false]);
            

call vs apply

function.call(thisArg, arg1, arg2, ...)

func.apply(thisArg, [argsArray])


const numbers = [1, 13, 8, 5, 3];

console.log(Math.max.apply(Math, numbers));
console.log(Math.max.call(Math, ...numbers));
console.log(Math.max(...numbers));
            

bind

fun.bind(thisArg, arg1, arg2, ...)


function cookPie(radius, slice) {
  console.log('#cookPie', this.type, radius, slice);
}

const lemonPie = { type: 'lemon', radius: 9 };

const cookLemonPie = cookPie.bind(lemonPie);
cookLemonPie(47, true);

const cookHugeLemonPie = cookPie.bind(lemonPie, Number.MAX_SAFE_INTEGER);
cookHugeLemonPie(false);
            

IIFE (Immediately Invoking Function Expressions)


function () {
  // code
}
          

(function () {
  // code
})
          

(function () {
  // code
})();
          

(function (arg0, arg1) {
  // code
})('arg', 1);
          

const lemonPie = {
  radius: 9,
  cook: (function () {
    const type = 'lemon';

    return function() {
      console.log('#cook', type, this.radius);
    };
  })(),
};
          

scope: static (lexical) vs dynamic

static (lexical) scope - if only by looking at the source code one can determine in which environment a binding is resolved

dynamic scope - if a caller defines an activation environment of a callee

JS implements static scope for all except this

static (lexical) scope


const pie = { type: 'lemon', radius: 91 };

function cookPie() {
  console.log('#cookPie', pie);
}

function cookMyPie() {
  const pie = { type: 'choco', radius: 87 };

  cookPie();
}

cookMyPie();
            

what's wrong with this? Or dynamic scope


function cook() {
  console.log('#cookPie', this.type, this.radius);
}

const lemonPie = { cook, type: 'lemon', radius: 46 };
const chocoPie = { cook, type: 'choco', radius: 44 };

lemonPie.cook();
chocoPie.cook();
            

fat arrow functions


(...args) => {
  return args;
}
            
  • no own this
  • no own arguments
  • cannot be used as constructors

() => {} this


const cook = () => {
  console.log('#cookPie', this.type, this.radius);
}

const lemonPie = { cook, type: 'lemon', radius: 46 };
const chocoPie = { cook, type: 'choco', radius: 44 };

lemonPie.cook();
chocoPie.cook();
            

const lemonPie = {
  type: 'lemon',
  radius: 46,
  cook: function() {
    const fatCook = () => { console.log('#fatCookPie', this); }

    fatCook();
  }
};

lemonPie.cook();
            

() => {} arguments


const cookPie = () => {
  console.log('#cookPie', arguments); // ReferenceError: arguments is not defined
}

cookPie('lemon', '55');
            

const cookPie = (...args) => {
  console.log('#cookPie', args);
}

cookPie('lemon', '55');
            

() => {} ctor


const Pie = (radius, height) => {
  this.radius = radius;
  this.height = height;
}

const pie = new Pie(20, 5); // TypeError: Pie is not a constructor
            

() => {} short syntax


const cookPie = (pie) => {
  return console.log('#cookPie', pie);
};
            

const cookPie = pie => console.log('#cookPie', pie);
            

const cookPie = (pie, factory) => factory.cook(pie);
            

const cookPie = pie => ({ error: 'Cannot cook 👨‍🍳' });
            

() => {} perfect fit


const pies = [
  { type: 'lemon', radius: 1 },
  { type: 'choco', radius: 4 },
  { type: 'lemon', radius: 2 },
];
            

pies.filter(function (pie) {
  return pie.radius > 2;
}).map(function(pie) {
  return { type: pie.type, crashed: true };
});
            

pies
  .filter(pie => pie.radius > 2)
  .map(pie => ({ type: pie.type, crashed: true }));
            

closure

A closure is the combination of a function and the lexical environment within which that function was declared


function makeCooker() {
  const name = 'Isaac';

  return function () {
    console.log(name);
  };
}

const cooker = makeCooker();
cooker();
            

function makeCooker(name) {
  return function (pie) {
    console.log(name, 'is going to cook', pie);
  };
}

const cooker = makeCooker('Albert');
cooker('lemon pie');
            

private members via closure


const lemonPie = (function() {
  const type = 'lemon';
  let radius = 0;

  return {
    inc: () => radius += 1,
    dec: () => radius -= 1,
    rad: () => radius,
  };
})();
            

closure in a loop


function cookPie(index) {
  console.log('#cookPie', index);
}
            

for (var i = 0; i < 5; i++) {
  setTimeout(() => {
    cookPie(i);
  }, i);
}
            

for (var i = 0; i < 5; i++) {
  setTimeout(((index) => {
    cookPie(i);
  })(i), i);
}
            

for (var i = 0; i < 5; i++) {
  let index = i;
  setTimeout(() => {
    cookPie(index);
  }, i);
}
            

closure in a loop


for (let i = 0; i < 5; i++) {
  setTimeout(() => {
    cookPie(index);
  }, i);
}
            

chaining


lemonPie.inc().inc().dec().rad();
          

const lemonPie = {
  type: 'lemon',
  radius: 6,
  inc: function () {
    this.radius += 1;

    return this;
  },
  dec: function () {
    this.radius -= 1;

    return this;
  },
  rad: function () {
    return this.radius;
  },
};
          

self-overwriting functions


getRadius(); // 2
getRadius(); // 4
getRadius(); // 6
          

function getRadius() {
  let radius = 0;

  getRadius = function() {
    return radius += 2;
  };

  return getRadius();
}
          

singleton


const factory1 = new PieFactory();
const factory2 = new PieFactory();

factory1 === factory2; // true
          

function PieFactory() {
  const instance = this;

  PieFactory = function() {
    return instance;
  }
}
          

decorator


function cookPie(type, radius, slice) {
  console.log('#cookPie', type, radius, slice);
}
            

function sendStats(func) {
  return function(...args) {
    console.log('#stats:', args);
    return func(...args);
  };
}
            

cookPie = sendStats(cookPie);

cookPie('lemon', 71, true);
            

decorator memoization


function cookPie(type, radius, slice) {
  console.log('#cookPie', type, radius, slice);
  return type;
}
            

function memoize(fun) {
  return function(...args) {
    const key = JSON.stringify(args);
    fun.memory = fun.memory || {};

    if (fun.memory[key]) {
      return fun.memory[key];
    }

    return fun.memory[key] = fun(...args);
  }
}
            

const memCookPie = memoize(cookPie);

console.log(memCookPie('lemon', 38, true));
console.log(memCookPie('choco', 38, true));
console.log(memCookPie('lemon', 38, true));
            

decorator spy


function cookPie(type, radius, slice) {
  console.log('#cookPie', type, radius, slice);
}
            

function spy(fun) {
  function spied(...args) {
    spied.callCount += 1;
    spied.args.push(args);

    return fun(...args);
  }

  spied.callCount = 0;
  spied.args = [];

  return spied;
}
            

const spyCookPie = spy(cookPie);
spyCookPie('lemon', 73, true);
spyCookPie('choco', 71, false);

console.log(spyCookPie.callCount, spyCookPie.args);
            

Thanks