import {NOTE} from './_util'

About functions in JavaScript

JavaScript was heavily influenced by Scheme, a functional programming language of the LISP lineage. You may not see it from the syntax, and the Wikipedia article on JavaScript explains why:

In 1995, [Netscape] recruited Brendan Eich with the goal of embedding the Scheme programming language into its Netscape Navigator. [...] Netscape Communications then decided that [JavaScript] would complement Java and should have a similar syntax[...]

Even though JavaScript doesn't quite resemble Scheme, functions in JavaScript are still among the most pleasant to work with, and we can definitely feel the functional nature of the language in many places.

You can write programs without any functions in JavaScript, but you would be missing out on a rich set of features that stem from the language's heritage. As you start tackling complexity in your code, regardless of whether you do so using object orientation or functional programming, functions will become essential (or unavoidable, depending on how you look at things). JavaScript functions have a few tricks up their sleeves that are useful to know, and we'll take a look at them in this module.

NOTE: Even though there does not seem to be a clear-cut consensus, we will use the word 'parameters' to mean 'the names that a function uses to refer to values that may be passed to it', and 'arguments' to refer to 'the values being passed to a function on invocation'.

Defining functions

Let's talk about syntax first.

There are four ways to define functions.

  • (1) function assigned to a variable or object property (a.k.a. function expression)
// 1a. anonymous function expression:
const foo = function (x, y, z) { /* ... */ }

// 1b. named function expression:
const fooNamed = function fooNamed(x, y, z) { /* ... */ }
  • (2) function that you don't assign (a.k.a. function declaration statement) and cannot be anonymous
function bar(x, y, z) { /* ... */ }
  • (3) the new arrow function expression that you assign to a variable
const baz = (x, y, z) => { /* ... */ }
  • (4) object property
const bam = {
  property(x, y, z) { /* ... */ }
}

The first two forms can be used more or less interchangeably. The only tiny difference is that the named functions can be defined anywhere in the scope, and they are treated as if they were defined at the very top of the scope (the module they are defined in, or a function in which they are defined):

whereDoYouComeFrom() // look at the bottom!

This characteristic of named functions is called 'hoisting'. Because these functions are hoisted, some programmers like to use them for utility functions that would only get in the way if defined before the business logic code.

In some versions of some JavaScript engines, it is possible to declare functions in an if block using function statements, even if the if condition is not met. You should, therefore, avoid using function statements in if blocks as it may lead to confusion and subtle bugs.

The arrow function form is a new syntax introduced in ECMAScript 6, and the one I prefer to use. There are subtle differences between arrow- and non-arrow functions in both syntax and semantics, though, and they can't be used interchangeably in all situations. We'll talk about the differences later, after we've covered the basics.

NOTE: In this module we'll call both function expressions and functions statements 'non-arrow functions', to distinguish them from arrow functions.

The last form, the object property, is syntactic sugar for defining a property and assigning a function expression to it. It is new in ES6, and it is the same as saying {property: function (x, y, z) { /* ... */ }}.

In all cases we have three parameters: x, y, and z. If you are used to languages where you can invoke functions using named arguments (keyword arguments), you should be aware that JavaScript does not have this feature. There are ways to work around that, but it is often times simpler to just work with positional arguments instead.

If you find that your parameter list is becoming a maintenance nightmare, consider passing a single object and accessing its properties instead. This is especially useful combined with argument destructuring discussed in this module.

Arrow functions

Arrow functions have a few syntax variations depending on the number of parameters and/or the complexity of the function body.

When there is only one parameter, the parenthesis can be omitted.

const noParens = x => { /* .... */ }

When the function body is a single expression, then the braces can be omitted.

const noBraces = x => x + 1

In the above form, if you wish to return an object, it must be wrapped in parenthesis.

const noBracesObject = x => ({foo: x})

You don't have to omit neither braces nor parenthesis if you don't want to, though. Keep in mind that, if you don't omit braces, you must use a return statement to return values.

const fullPackage = (x) => { return x + 1 }

Except for single-expression arrow functions, you generally have to use the return statement to return a value. If you don't, undefined is returned instead.

const returning = function (x) {
  return x + 1
}

Destructuring function arguments

Starting with ES6, function arguments can be destructured using parameters with pattern-matching. We'll go over a few examples of how this destructuring works:

const takesArray = ([x, y, z]) => {
  console.log('took array:', x, y, z)
}
takesArray([1, 2, 3]) // took array: 1 2 3

const takesArrayWithSplat = ([head, ...tail]) => {
  console.log('took array with splat:', head, tail)
}
takesArrayWithSplat([1, 2, 3]) // took array with splat: 1 [ 2, 3 ]

const takesObj = ({x, y, z}) => {
  console.log('took obj:', x, y, z)
}
takesObj({x: 'foo', y: 'bar', z: 'baz'}) // took obj: foo bar baz

const takesComplex = ({x: [y, z], w}) => {
  // x is not used here because it was mapped to an array containing y and z
  console.log('took complex:', y, z, w)
}
takesComplex({x: [1, 2], w: 3}) // took complex: 1 2 3

const remapsKeys = ({x: foo, y: bar}) => {
  console.log('remapped:', foo, bar)
}
remapsKeys({x: 'x', y: 'y'}) // remapped: x y

NOTE: Although destructuring may seem like a very neat trick, you should keep in mind that it may sometimes obfuscate the function signature to the point where you cannot recall its original intent. Try thinking about the advantages of destructuring arguments on case-by-case basis before you jump on using them.

Default parameter values

As of ES6, default values of parameters can also be specified.

const havingDefault = (x = 12) => {
  console.log(x)
}
NOTE('Invoking havingDefault with an argument')
havingDefault(10) // 10
NOTE('Invoking havingDefault without an argument')
havingDefault() // 12

Optional arguments and splats

JavaScript functions do not have any restrictions on the number of arguments you can pass. You can call the same function with as many or as little arguments as you want, and it always works (sort of). The flip side is that there is no way to define required parameters nor are exceptions thrown when arguments are missing. Any parameters for which no arguments are passed will be undefined.

const noArg = (x) => {
  console.log(x)
}

NOTE('Invoking noArg without arguments')
noArg() // undefined
NOTE('Invoking noArg with more arguments than parameters')
noArg(1, 2, 3, 4) // 1 (other arguments are silently ignored)

This is in just how JavaScript does this. Whether you like it or not, it's a language feature. Resistance is undefined!

The extra arguments passed to a function can be trapped using splats, which appeared in ES6 for the first time. These are also known as 'rest parameters'.

const withSplat = (x, ...rest) => {
  console.log(x, rest)
}

NOTE('Invoking withSplat(1, 2, 3)')
withSplat(1, 2, 3) // 1 [ 2, 3 ]

Splats are an empty array if there are no extra arguments.

NOTE('Invoking withSplat(1)')
withSplat(1) // 1 [ ]

Functions as object properties (a.k.a. methods)

You have already seen that function expressions can be assigned to variables. They can also be defined as and assigned to object properties. As mentioned, there is no difference between defining a callable property using the short-hand notation and assigning a function expression.

Merely defining as, and assigning to, object properties does not make functions special. They are still the same functions. You will find more details about how functions are effectively used as object properties (methods) in the this module, but try to read this module to the end before going there. For now, let's continue with the examples:

const property = () => console.log("I'm a property")
const proprietor = {
  prop: property
}
NOTE('Invoking proprietor.prop()')
proprietor.prop() // I'm a property

const proprietor2 = {
  prop() { console.log("I'm a property") }
}
NOTE('Invoking proprietor2.prop()')
proprietor2.prop() // I'm a property

Higher-order functions

Functions can be used as arguments to other functions, and returned from them. Functions that receive other functions as arguments or return functions (or both) are called higher-order functions. Higher-order functions allow for great composability.

const is = x => y => x === y
const isTwo = is(2)
const not = fn => x => ! fn(x)
const notTwo = not(isTwo)
console.log('notTwo(1) === ' + notTwo(1)) // true
console.log('notTwo(2) === ' + notTwo(2)) // false

When a value is passed to is(), a new function is returned which will compare its argument to the value that was passed to is(). The isTwo() function is created by invoking is() with 2 as the value. The not() function takes a function fn, and returns a function that, given an argument, calls fn() with that argument and returns the negated return value. We combined the two functions to create an opposite of isTwo(): notTwo(). Although these examples are quite trivial, the way we can compose functions can scale to much more complex problems without making things much more difficult.

On the other hand, don't fall into a trap of breaking everything down into tiny functions and write a big function using bazillion of them. It ultimately makes the code harder to follow.

Closures

Functions in JavaScript allow us to build closures. When a function is defined within another function, the inner function has access to the outer function's scope (variables defined in the outer function as well as the outer function's arguments), even after it is returned and invoked outside of the outer function. Furthermore, the scope of the outer function is private (not accessible to anything outside it except the inner function's scope). In this configuration, we say that the outer function 'closes over' the inner one. We have seen this with is() and not() already, where the returned function retain access to the outer function's arguments. Let's do one more example where the inner function uses a variable defined in the outer function.

const makeRequester = () => {
  // This could be generated dynamically for example:
  const token = {
    code: '1234567',
    username: 'bob'
  }
  return (x) => {
    console.log(`Request ${x} for ${token.username} using code ${token.code}`)
  }
}
const requester = makeRequester()
requester('hamburger') // Request hamburger for bob using code 1234567

In the above example, the token is generated within makeRequester()'s closure, and is completely inaccessible to the outside world. This effectively makes it tamper-free.

Immediately-invoked function expressions

Immediately-invoked function expressions (IIFE for short) are function expressions that are... well... immediately invoked. A stand-alone (i.e., unassigned) function expression is constructed by enclosing a function expression in parenthesis, and then invoked by adding another set of parenthesis right after the ones around the function. This works with both the function expression that uses the function keyword, and with arrow function expressions. Here's an example:

;(() => {
  console.log("I'm invoked immediately!")
})()

NOTE: We use a semi-colon before the opening parenthesis to avoid the automatic semi-colon insertion (ASI) from interfering with our code. Not doing this would have cause the JavaScript engine to interpret our code as requester('hamburger')(() => { ... })(). If you find that ugly, you could also use semi-colons everywhere at the end of each statement, or just the preceding statements.

IIFE's return value can be assigned to a variable.

const requester2 = ((username) => {
  // This could be generated dynamically, for example
  const token = {
    code: '1234567',
    username: username
  }
  return (x) => {
    console.log(`Request ${x} for ${token.username} using code ${token.code}`)
  }
})('john')
requester2('beer') // Request beer for john using code 1234567

In the above example, we have converted the makeRequester() function into an IIFE, which generates the requester function on the fly, and its return value is assigned to requester2().

An IIFE is only ever invoked once, so this pattern is commonly used for initialization before returning functions or other objects, or to create private state that should not be accessible to the outside code.

Functions are objects

Let me remind you again that JavaScript functions are objects. This is quite evident when you start noticing that you can define properties on them.

const funcWithProps = () => { /* ... */ }
funcWithProps.someProp = 'foo'

Although user-defined properties on functions are not all that useful, functions also have a few built-in properties that are. You can use the length property to figure out the number of parameters it expects.

const howMany = (a, b, c) => { /* ... */ }
console.log('howMany.length === ' + howMany.length) // 3

We could, for example, use the length property to implement a function that, given a function, accumulates arguments until there are enough (or more) of them to satisfy the original function's parameter list.

const accumulate = (fn, accumulated = []) => {
  // Immediately return a function
  return (...args) => {
    // Add the new args to the accumulated args
    const allArgs = accumulated.concat(args)
    if (allArgs.length < fn.length) {
      // We don't have enough args, so return a new accumulator function which
      // starts from the currently accumulated arguments.
      return accumulate(fn, allArgs)
    }
    // we have all the arguments we need (or more), so we can now call the
    // original function.
    return fn(...allArgs)
  }
}

const printThree = (x, y, z) => {
  console.log(x, y, z)
}

const printWhenThree = accumulate(printThree)

NOTE('printWhenThree(1)')
let next = printWhenThree(1) // (no output)

NOTE('printWhenThree(2)')
next = next(2) // (no output)

NOTE('printWhenThree(3)')
next(3) // 1 2 3

In the accumulate() function, we have quasi-recursion, where the inner function invokes the outer function passing the state (array of arguments accumulated thus far). It is not real recursion because the call happens after the user has invoked the inner function.

NOTE: Yes, accumulate() is cute and not very useful in real life. See the bind() function used later on that is useful.

A function's name property will tell us what the function is named.

console.log('howMany.name === ' + howMany.name) // howMany

While it may sound silly, it can be useful in some cases. For example, if you are writing code that sets up routing, you can encode the route name in the function name.

const router = (function () {
  const routes = {}
  return {
    add(fn) {
      routes[`/${fn.name.toLowerCase()}/`] = fn
    },
    show() {
      console.log(routes)
    },
  }
})()

const home = () => {}
router.add(home)
router.show() // { '/home/': [Function: home] }

You can also call the toString() property to get the source representation of a function.

console.log(howMany.toString()) // (a, b, c) => {/* ... */}

NOTE: The output is generally going to be different from the original function in terms of formatting, because it is decompiled from the JavaScript byte code, and not a reference to your source code. If you are running this code in NodeJS with Babel transpiler, it will, of course, be completely different because it is compiled into a non-arrow function expression.

There are several more properties like call(), apply(), or bind(), but we will only talk about bind() in this module. call() and apply() are discussed in the this module.

The bind() function and partial application

All functions have a bind() function. This function is normally used to bind non-arrow functions' this to some value, but it can also be used to perform partial application of functions. For instance:

const onlyNeedsThree = (a, b, c) => {
  console.log(a, b, c)
}

const onlyNeedsOne = onlyNeedsThree.bind(undefined, 1, 2)

NOTE('Calling partially applied onlyNeedsTree')
onlyNeedsOne(3) // 1 2 3

The first argument to the bind() call is going to be the value of this in an non-arrow function. With arrow functions, we don't really care, so we leave it as undefined. Since this may look a little confusing, we could write our own function to partially apply.

const partial = (fn, ...args) => fn.bind(undefined, ...args)

const onlyNeedsTwo = partial(onlyNeedsThree, 'a')

NOTE('Calling partially applied onlyNeedsThree')
onlyNeedsTwo('b', 'c') // a b c

Arrow vs non-arrow functions

You may be wondering why there are two kinds of functions in JavaScript (arrow and non-arrow). The function function has a few characteristics that the arrow function does not. Let's take a look at what those are.

Non-arrow functions have two special constants that are accessible within their scope. These objects are this, and arguments.

The arguments object is an array-like object that contains all the arguments passed to a function (regardless of its parameters). It is array-like because it is not truly an array, and lacks some of the functions that real array objects have. We won't go into further discussion of this object, as ES6 offers much more elegant ways to get equivalent functionality, but if you ever run into weird code like [].slice.call(arguments) you should know that this is because arguments is only array-like.

The this object is convoluted enough to deserve a module of its own. Please refer to this module for more information.

Furthermore, non-arrow functions can be used as object constructors, a topic that is covered in more detail in the prototypal inheritance module.

As mentioned before, arrow functions do not have arguments and this (and also some other things that are not that important). Some programmers may tell you that the arrow function has this which is bound to this in the outer scope, but that is only true for practical purposes. You can access this in an outer non-arrow function, for sure, but if your arrow function is defined outside non-arrow functions, it will not* have access to any kind of this from any scope. If you care about this at all, you probably want to use non-arrow functions instead and avoid having to think about whether this means this or that (pun intentional).

If you want to restrict yourself to one of the function forms, you can pick the arrow functions if you are willing to forget about this and constructors (which may not be a bad thing, judging from what people who have done so say), or forget about arrow functions. You could always use both, and you should definitely know both, though.

In conclusion

Hopefully this gives you enough material to continue exploring programming with functions in more detail. Where functions really shine is when you program using functional programming style (surprise!). If you are curious about how that works, take a look at the declarative programming module.

// This function is intentionally hanging down here
function whereDoYouComeFrom() {
  console.log('Look at the bottom!')
}

results matching ""

    No results matching ""