Function declarations versus arrow functions

JavaScript V8 adds the arrow function declarator from modern JavaScript. This is a handy shorthand but it’s more than just that. There are some behavioral differences too that you’ll need to understand before diving in. The old way of declaring functions still exist of course, and there will always be a need for it.

You’ll be used to declaring functions like this

function yourFunction () {
// some code
}

or like this

var yourFunction = function () { 
  // some code 
}

Before diving into the new syntax let’s take a closer look at some of the behaviors the function syntax. It’s important to be completely comfortable with it, as arrow functions behave differently.

We’ll use this data, but avoid repeating it for every example.

 var data = [{ 
    firstName:'John', 
    lastName:'Smith' 
  },{
    firstName:'Jane', 
    lastName:'Doe' 
}];

JavaScript V8 Function hoisting

The key difference between the two above methods is related to whether or not you have to declare it before referencing it.

These two are equivalent – in one case the function is declared before it is used, yet they both work.

function hoisting () {
  function inputLength (input) {
    return input.length
  }
  Logger.log(inputLength(data))
}
function hoisting2 () {
  Logger.log(inputLength(data))
  function inputLength (input) {
    return input.length
  } 
}

Whereas in these examples,  hoisting4 fails with TypeError: inputLength is not a function

function hoisting3 () {
  var inputLength = function (input) {
    return input.length
  } 
  Logger.log(inputLength(data))
}
function hoisting4 () {
  Logger.log(inputLength(data))
  var inputLength = function (input) {
    return input.length
  } 
}

Why? Because JavaScript processes function declarations first and ‘hoists’ them as if they were declared at the beginning. Since inputLength is not declared yet in hoisting4, then it fails. But wait, why that error inputLength is not a function rather than Cannot access ‘inputLength’ before initialization which is what you’d get if you used const rather than var as below

function hoisting5 () {
  Logger.log(inputLength(data))
  const inputLength = function (input) {
    return input.length
  } 
}

The answer is that both var and function are ‘hoisted’, but as we saw in v8 scopes var can redefine the value of a variable, so all vars in in your function start with a value of undefined (as opposed to simply not yet having been defined as would be the case with let and var). This is another reason why one of my golden rules of JavaScript is to avoid using var. I’d rather be alerted by the IDE that I’m referencing something I haven’t mentioned yet than try to track down why a value is unexpectedly undefined.

JavaScript V8: A note on semicolons

I’ve always been an advocate of putting the semicolons in, regardless of whether the IDE or linter you are using demands it. With the brevity and cleanliness of Arrow functions and other things I’ll cover in future posts, it seems untidy and unnecessary, so I’m changing the habit of a lifetime and jettisoning semicolons altogether, as you’ll see in these examples.

JavaScript V8: Arrow functions

Now let’s look at the syntactical changes, as well the behavior changes that we get with Arrow functions. Spoiler – we’ll always need the function syntax for some use cases in the foreseeable future.

In principle, these are equivalent. Note there is no hoisting so you need to declare functions before you use them.

const myArrow = () => {
  // .. do something
}
var myArrow = function () {
  // .. do something
}

JavaScript V8: Arguments

No surprises here

const myArrowArgs = (a,b) => {
  // .. do something
  return r
}

If only 1 argument, you can omit the parenthesis

const myArrowArg = a => {
  // .. do something
  return result
}

There are a host of other goodies around arguments, but before covering them I’ll need to write about destructuring, so I’ll come back to those in another v8 article.

JavaScript V8: Return values

Arrow functions have an implied return in functions that only have one line of code, so this

const myArrowReturn = a => a+1

is just shorthand for this

const myArrowReturn = (a) => {
  return a + 1
}

Arrow functions can be handy for array functions in particular, where something like this

function myArrowUpperVerbose (array) {
  return array.filter(function (d) {
    return d
  })
  .map(function (d) {
    return d.toUpperCase()
  })
  .join('')
} 
function runmad () {
  // 'AB'
  Logger.log(myArrowUpperVerbose(['a','', 'b']))
}

can be more succinctly and meaningfully expressed as

const myArrowUpper = array => 
  array.filter(d => d).map(d => d.toUpperCase()).join('')

JavaScript V8: Returning objects

Because of the optionality of curly brackets around the body of a function, returning an object like this

const myArrowObReturn = () => {
  return {
    firstName: 'John',
    lastName: 'Doe'
  }
}

would confuse an IDE, but you can still omit the return by simply surrounding the returned object in brackets

const myArrowOb = () => ({
  firstName: 'John',
  lastName: 'Doe'
})

JavaScript V8: self, this, that and the other

Let’s get back to legacy Apps Script for a bit and take a look at what ‘this’ points to. For our example data has been defined globally

 var data = [{ 
    firstName:'John', 
    lastName:'Smith' 
  },{
    firstName:'Jane', 
    lastName:'Doe' 
}];

function thisData () {
  // [{lastName=Smith, firstName=John}, {lastName=Doe, firstName=Jane}]
  Logger.log(this.data)
}

so this refers to the global space and finds the value of data correctly. However, when instead of var, we use const or let to define data we find that this.data is undefined. So what’s going on here? Digging into the chrome debugger (which is same v8 engine), we find that this contains another reference this.globalThis,  and we have to look under this.globalThis to find data if defined using const or let, but  this if defined using var.

// when using let or const instead of var
function thisData () {
  // null
  Logger.log(this.data)
  // [{lastName=Smith, firstName=John}, {lastName=Doe, firstName=Jane}]
  Logger.log(this.globalThis.data)
}

If you are using this to rely on referencing stuff in the global space (for example I do that to refer to items outside namespaces in Organizing asynchronous calls to google.script.run), then be careful how you define them.

However this is usually used inside object or event definitions, where this refers to the object in which the function is declared

function thisObject () {
  const ob = { 
    firstName:'John', 
    lastName:'Smith',
    fullName: function () {
      return this.firstName + ' ' + this.lastName
    } 
  }
  // John Smith
  Logger.log(ob.fullName())
}

Similarly, when using function with the new keyword, this refers to the enclosing function.

function Friend (firstName, lastName) {
  this.firstName = firstName
  this.lastName = lastName
  this.initials = function () {
    return this.firstName.slice(0,1) + this.lastName.slice(0,1)
  }
}
function makeFriend () {
  const f = new Friend('john','doe')
  // 'jd'
  Logger.log(f.initials())
}

Using the new keyword causes the function to return the new instance of the object by default, so this will return the same thing with or without  return this

function Friend (firstName, lastName) {   
  this.firstName = firstName   
  this.lastName = lastName  
  this.initials = function () { 
    return this.firstName.slice(0,1) + this.lastName.slice(0,1)   
  }
  return this
}

You can return something other than this too – Here only the initials function is being exposed.

function SlimFriend (firstName, lastName) {   
  const initials = function () { 
    return firstName.slice(0,1) + lastName.slice(0,1)   
  }
  return {
    initials: initials
  }
}

Arrow function constructors

You cannot use the new keyword with an arrow function. Consider this rewrite of the Friend function, using the arrow syntax. It will return ‘ArrowFriend is not a constructor’

const ArrowFriend = (firstName, lastName) => {   
  this.firstName = firstName   
  this.lastName = lastName  
  this.initials = function () { 
    this.firstName.slice(0,1) + this.lastName.slice(0,1)   
  }
}
const makeArrowFriend  = () => {
  // ArrowFriend is not a constructor
  const f = new ArrowFriend('john','doe')
  Logger.log(f.initials())
}

Classes

Using a function as a constructor for an instance of an object has always felt a little clunky in JavaScript, and V8 allows it to come into line with most other languages by introducing the the concept of a class. Here is the Friend function, rewritten as a class. initials is a method and constructor does the initial setup of the instance during the creation

class ClassFriend {
  constructor(firstName, lastName) {
    this.firstName = firstName   
    this.lastName = lastName  
  }
  initials () {
    return this.firstName.slice(0,1) + this.lastName.slice(0,1)  
  }
}
const makeClassFriend  = () => {
  const f = new ClassFriend('john','doe')
  Logger.log(f.initials())
}

JavaScript V8: set and get

If you used Object.create in legacy Apps Script, you’ll be used to the idea of setters and getters to interact with property values. classes allow this too. Let’s rewrite this, exposing some of the properties and changing the method to a get. Since we are using get and set, we need an alternative internal variable to store these in. Typically, the same name, but with a leading underscore is used.

class ClassFriend {

  constructor(firstName, lastName) {
    this._firstName = firstName   
    this._lastName = lastName  
  }
  get initials () {
    return this._firstName.slice(0,1) + this._lastName.slice(0,1)  
  }
  set firstName (val) {
    this._firstName = val
  }
  get firstName () {
    return this._firstName
  }
}

Now we can set and get property values like this

const makeClassFriend  = () => {
  const f = new ClassFriend('john','doe')
  // jd john
  Logger.log(f.initials, f.firstName)
  f.firstName = "donald"
  // dd donald
  Logger.log(f.initials, f.firstName)
}

One downside of all of this is that private variables are not available in a class in ES6, so for example the property this._firstName is visible from outside. ES9 makes provision for private class variables, but ES9 is not generally available yet, and certainly not in Apps Script V8. There are various workarounds for this, but they are all fairly ugly so I won’t bother going into them here. If you need private variables in your classes, maybe sticking with the function style constructor is the best way for now. There a few other class features such as subclassing, but I’ll cover those in a future, more advanced article

This, that and self

If you are defining other objects inside your constructor, things are going to get a little more complex. Remember that this refers to the innermost enclosing object. Consider this example

function SelfFriend (firstName, lastName,salutation) { 
  this.firstName = firstName
  this.lastName = lastName

  this.who = {
    salutation: salutation,
    initials: function () {
      // this is wrong
      return this.salutation + ' ' + this.firstName.slice(0,1) + this.lastName.slice(0,1)
    }
  }
}

function makeFriend () {
  const f =  new SelfFriend('john','doe','mr')
  console.log(f.who.initials())
}
makeFriend()

When we define initials inside this.who, this is now pointing at the innermost function, i.e. this.who, as opposed to the SelfFriend function – so this.firstName and this.lastName are undefined, but this.salutation is correct

self  is generally used to deal with this situation, where self refers to the parent function. The above can be re-written as follows. Occasionally, you’ll see that being used instead of self but I prefer to use self inside such constructor functions and that in event handler callbacks, which will have a similar issue

function SelfFriend (firstName, lastName,salutation) { 
  const self = this
  self.firstName = firstName
  self.lastName = lastName

  self.who = {
    salutation: salutation,
    initials: function () {
      return this.salutation + ' ' + self.firstName.slice(0,1) + self.lastName.slice(0,1)
    }
  }
}

this and arrow functions

The confusing (but occasionally helpful) scope of this in JavaScript is cleaned up in arrow functions. Let’s look at this snippet.

const arrowThis = () => {
  const ob = {
    first: 'john',
    last: 'smith',
    func: function(prefix) {
      return prefix + ' ' + this.first + ' ' + this.last
    },
    // BUT ... this will be wrong
    arrow: prefix => prefix + ' ' + this.first + ' ' + this.last
  }
  // mr john smith
  Logger.log(ob.first + ':' + ob.last + ':' + ob.func('mr'))
  // john:smith:mr undefined undefined
  Logger.log(ob.first + ':' + ob.last + ':' + ob.arrow('mr'))
  // mr john smith
  Logger.log(ob.first + ':' + ob.last + ':' + ob.func.call(ob,'mr'))
  // john:smith:mr undefined undefined
  Logger.log(ob.first + ':' + ob.last + ':' + ob.arrow.call(ob,'mr'))
  // john:smith:mr undefined undefined
  Logger.log(ob.first + ':' + ob.last + ':' + ob.func.call(this,'mr'))
}

From what we’ve seen so far, the methods func and arrow should be the same, but they are not, because arrow will treat this as referring to global this (which is why this.first and this.last are undefined), whereas func will correctly refer to this as the object in which it is defined.

Even using .call to reassign the behavior of this doesn’t work with arrow functions. The first argument to call is ignored. Note then, that ob.func.call(this,..) will behave the same as ob.arrow(..)

Summary

Arrow functions are about brevity, but they are far from being just syntactic sugar. Note especially the behavior around

  • the new keyword
  • this, call, bind, apply and prototype
  • private variables in constructed objects

But they will very quickly feel more natural. Here’s a typical pattern for converting sheet values into objects, in legacy Apps Script

function legacyOb () {
  // sheet style values
  var values = [['first','last'],['john','smith'],['jane','doe']];
  // convert to object
  var heads = values[0];
  var data = values.slice(1);  
  var sheetOb = data.map(function (row) {
    return row.reduce (function (p,col,index) {
      p[heads[index]] = col;
      return p;
    }, {});
  });
  //  [{"first":"john","last":"smith"},{"first":"jane","last":"doe"}]
  Logger.log(JSON.stringify(sheetOb));
}

And the same thing, but with arrow functions

const arrowObBriefer = () => {
  const values = [['first','last'],['john','smith'],['jane','doe']]
  const sheetOb = values.slice(1).map(row => row.reduce ( (p,col,index) => {
      p[values[0][index]] = col
      return p
  }, {}))
  //  [{"first":"john","last":"smith"},{"first":"jane","last":"doe"}]
  Logger.log(JSON.stringify(sheetOb));
}

Next, I’ll cover destructuring, which will open up some great new features for function arguments.