Apps Script V8 adds destructuring from JavaScript ES6. Legacy Apps Script already had destructuring of arrays added fairly recently, but v8 gives full a destructuring capability.
Destructuring: What is it?
It’s a way of plucking elements or property values from arrays or objects, assigning them to individual variables so they can be handled more easily and concisely.
Deconstructing arrays
Legacy Apps Script already allows you to do a subset of this, where elements are positionally extracted from an array – in this case the kind of data you’d get back from a getValues() on a sheet
var yourFunction = function () { const values = [['first','last'],['john','smith'],['jane','doe']]; const [headers,john,jane] = values // [first, last] Logger.log(headers) // [john, smith] Logger.log(john) // [jane, doe] Logger.log(jane) }
With V8, you also get the …rest syntax which allows you to assign everything else in the array that you haven’t specifically assigned to other variables.
const rarray = () => { const values = [['first','last'],['john','smith'],['jane','doe']]; const [headers,...data] = values Logger.log(headers) // [[john, smith], [jane, doe]] Logger.log(data) }
That’s equivalent (but rather more meaningful) to something like this that you may have done in legacy Apps Script
function larray () { var values = [['first','last'],['john','smith'],['jane','doe']]; var headers = values.slice(0,1)[0] var data = values.slice(1) // [first, last] Logger.log(headers) // [[john, smith], [jane, doe]] Logger.log(data) }
let and const
These behave exactly as normal, so
const [headers,...data] = values // this would be an error const headers = 'something else' // as would this headers = 'another thing' let [headers,...data] = values // this would be an error let headers = 'something else' // this would be ok headers = 'another thing'
Destructuring objects
Completely new in V8 is the ability to deconstruct objects. You can declare new variables the same as the property names of the object you want to extract from.
const dob = () => { const john = { firstName:'John', middleName: 'Eustace', lastName:'Smith', } const { firstName, lastName } = john // John Smith Logger.log(firstName + ' ' + lastName) }
Assigning alternative names
The variables you declare, by default, have the same name as the property from which they were extracted, but you can change that too.
const data = [{ firstName:'John', middleName: 'Eustace', lastName:'Smith', },{ firstName:'Jane', lastName:'Doe' }]; // deconstruct the array const [john, jane] = data // provide alternative variable names const { firstName: johnFirst, lastName: johnLast } = john const { firstName: janeFirst, lastName: janeLast } = jane // John Smith Logger.log(johnFirst + ' ' + johnLast) // Jane Doe Logger.log(janeFirst + ' ' + janeLast) }
Default values
Newly declared variables can have default values in both array and object destructuring. This examples combines all those possibilities.
const dobd = () => { const data = [{ firstName:'John', middleName: 'Eustace', lastName:'Smith', },{ firstName:'Jane', lastName:'Doe' }]; // deconstruct the array const [john, jane, bob = {firstName:'captain', lastName:'anonymous'}] = data // provide alternative variable names const { firstName: johnFirst, lastName: johnLast, middleName: johnMiddle = "thefirst" } = john const { firstName: janeFirst, lastName: janeLast, middleName: janeMiddle = "thesecond" } = jane const { firstName: bobFirst, lastName: bobLast, middleName: bobMiddle = "thebob" } = bob // John Eustace Smith Logger.log(johnFirst + ' ' + johnMiddle + ' ' +johnLast) // Jane thesecond Doe Logger.log(janeFirst + ' ' + janeMiddle + ' ' + janeLast) // captain thebob anonymous Logger.log(bobFirst + ' ' + bobMiddle + ' ' + bobLast) }
object …rest
Just as with arrays, you can use …rest with objects.
const dobe = () => { const data = [{ firstName:'John', middleName: 'Eustace', lastName:'Smith', },{ firstName:'Jane', lastName:'Doe' }]; // deconstruct the array const [john, jane] = data // pull out one item, and keep the rest as an object const {lastName,...forenames} = john // Smith Logger.log(lastName) // {middleName=Eustace, firstName=John} Logger.log(forenames) }
Skipping
You may not want every element of an array, so you can simply miss out variable names for the elements you don’t want.
const dobf = () => { const data = [{ firstName:'John', middleName: 'Eustace', lastName:'Smith', },{ firstName:'Jane', lastName:'Doe' }, { firstName:'Bob', lastName:'Green' }]; // just want the 3rd element const [,,bob] = data // pull out one item, and keep the rest as an object const {lastName,...forenames} = bob // Green Logger.log(lastName) // {firstName=Bob} Logger.log(forenames) }
Combining destructuring arrays and objects
All of those things can be combined, like this
const dobg = () => { const data = [{ firstName:'John', middleName: 'Eustace', lastName:'Smith', },{ firstName:'Jane', lastName:'Doe' }, { firstName:'Bob', lastName:'Green' }]; // just want the firstName and middlename property of the 3rd element const [,,{firstName: whoIsBob, middleName: bobMiddle = "the bob has no middle"}] = data // Bob the bob has no middle Logger.log(whoIsBob + ' ' + bobMiddle) }
Array arguments and destructuring
One of the big wins for destructuring is the role it plays in decluttering array arguments. Now the arguments themselves can be destructured.
For all these examples, I’m using the usual data below, which I won’t bother repeating from now on.
const data = [{ firstName:'John', middleName: 'Eustace', lastName:'Smith', },{ firstName:'Jane', lastName:'Doe' }, { firstName:'Bob', lastName:'Green' }];
Passing as a plain argument.
This passes the thing that needs to be logged to the logger and is simple when only one or a few arguments.
const loga = (firstName) => { Logger.log(firstName) } // passes the variable to be logged // John, Jane, Bob data.forEach(f=>loga(f.firstName))
Passing as an undestructured object
Another alternative is to pass the object, and the logger knows which property to extract
const logb = (ob) => { Logger.log(ob.firstName) } // passes the variable to be logged // John, Jane, Bob data.forEach(f=>logb) // or even more concisely data.forEach(logb)
Destructing in the argument list
In this model, everything discussed destructuring (including renaming, defaulting, skipping) above can be achieved in the argument list of the receiving function.
const logc = ({ firstName }) => { Logger.log(firstName) } // John, Jane, Bob data.forEach(logc)
This may not seem huge with only a few arguments, but one of the common errors in functions with many arguments is getting them in the wrong order or having to provide values for optional arguments. With destructuring the order is irrelevant, and you only need to pick out the values that are required.
Variable number of arguments
Dealing with an unknown number of variables in functions has always been a bit of a hack. A function declared with function has a special variable called arguments available for use, which is an array-like object which can be interrogated to figure out what arguments were passed. Before you can use it though, you have to convert it into a genuine array – which we can by using the slice method from the Array prototype – as below. Once that’s done, we can proceed normally, treating the argument list as an array.
const logc = ({ firstName }) => { Logger.log(firstName) } function loge () { const args = Array.prototype.slice.apply(arguments) args.forEach(logc) } const [john,,bob] = data loge(john,bob)
The arguments variable doesn’t exist in arrow functions, but there is something better. Spread syntax.
const logc = ({ firstName }) => { Logger.log(firstName) } const logd = (...args) => { args.forEach(logc) } const [john,,bob] = data loge(john,bob)
Spread syntax
We’ve covered some of the ideas of spread syntax already (it’s like …rest but in reverse), but let’s look a little deeper. Just as in destructuring, spread syntax applies to both arrays and object literals.
Copying an array
These are all equivalent
const c1 = data.slice() const c2 = data.map(f=>f) const c3 = [...data] Logger.log(c1) Logger.log(c2) Logger.log(c3)
As are these
const c4 = c1.concat(c2,c3) const c5 = [...c1,...c2,...c3]
Copying an object literal
const otherJane = {...jane}
Copying and replacing properties
const betterJane = {...jane,firstName: 'Better Jane',salution: 'Ms'} // {lastName=Doe, salution=Ms, firstName=Better Jane} Logger.log(betterJane)
Note that spreading doesn’t make a deep copy of the object. Consider this example
const cob = { firstName:'John', lastName:'Doe', relatedTo: [{ person: jane, relationship: 'brother' }] } const doe = {...cob} console.log(JSON.stringify(doe))
Gives
{ "firstName": "John", "lastName": "Doe", "relatedTo": [{ "person": { "firstName": "Jane", "lastName": "Doe" }, "relationship": "brother" }] }
If you then amend jane the difference will be reflected in the doe object
jane.middleName = 'adding a middle name' console.log(JSON.stringify(doe))
Like this
{ "firstName": "John", "lastName": "Doe", "relatedTo": [{ "person": { "firstName": "Jane", "lastName": "Doe", "middleName": "adding a middle name" }, "relationship": "brother" }] }
So spreading will only copy pointers to other objects, not clone the object themselves.
Summary
More great cleaning up of JavaScript courtesy of ES6. These destructuring and spreading capabilities, which at first may again seem a little like syntactic sugar, have contributed greatly to the development of state management frameworks such as Vuex and Redux for client side apps. V8 brings some of that cleanliness to Apps Script.