A bit of a monologue about various syntax changes and adjustments in learning and using the tool.
I know. Everyone knows.
That's why I wrote a library that writes to a spreadsheet instead. Ever since I started using it I'm far more productive. I don't know how people are still putting with that god-awful lagging logger.
If you want to see the details of changes to the language, in all of its geeky details, head on over to here.
Note that modules are not included, so all that import
goodness doesn't apply to the V8 runtime.
I won't go through normal, everyday usage, but will instead highlight what might be less common but quite handy when needed.
const noun = 'World';
const hello = `Hello ${noun}`;
I want to do generalize something like this to work in any case:
function output (string, noun) {
Logger.log( /* `Hello ${noun}` */ )
}
function entrypoint () {
const string = 'Hello ${noun}'; // just a regular string
output(string, 'World');
}
In other words, use a normal string, pass it some other part of code that can take the ${var}
pattern and apply it there. Why do I want this? Not only because it is cool, but because it allows me to de-couple code. I can define the string template in one place, but apply the template in another.
The function I use for this ability is here, which I totally didn't write myself:
function interpolate (targetString, params) {
const names = Object.keys(params);
const vals = Object.values(params);
return new Function(...names, `return \`${targetString}\`;`)(...vals);
}
It is used like so:
const output = interpolate('${greeting} ${noun}', {greeting: 'Hello', noun: 'World'});
Logger.log(output); // "Hello World"
Understanding the mechanics of how interpolate
works is beyond the scope of this gist, and beyond what you really need to use it (although it's fun once you've groked it).
In any case, now you can define a template in one place and then apply it in another.
Arrow functions are fun and dandy, but there are some serious side effects around, and if you're not aware of them can make debugging very frustrating.
The main gotcha has to do with the behaviour of the this
keyword inside one of these babies. For long-form function myFunction() {}
function, inside the body the this
is referring to itself, which is different from what this
is outside of the function.
For an arrow function, the this
value is the same both inside and outside of it.
Upshot, if you aren't using this
anywhere, you don't need to know much about it, but then you won't be able to use classes. (You do want to use classes, see below.)
You have two choices when an arrow function does not have any parameters:
- Empty parentheses
- Use the underscore
_
variable
// empty parentheses:
() => {}
// underscore variable
_ => {}
I prefer the latter, because Javascript has enough parentheses as it is, and it usually hurts my eyes whenever I see it.
And it should be noted that using a single _
is no special meaning in JavaScript: It is actually a legal variable name. In many languages, the _
is used to refer to a placeholder variable that is never used. So in this case, we are actually defining an arrow function with one parameter; it is just never used.
Arrow functions can optionally have a {}
block, in which case you don't have a return statement. But if you return an object, we have a confusing syntax:
_ => {obj: 'obj'}
That looks a lot like a body {}
rather than an object literal. So, you're supposed to use parentheses (told you there are a lot of parentheses) instead:
_ => ({obj: 'obj'})
Sigh.
There is nothing on its own wrong with var
, but I think it's worth "overcompensating" when transitioning to V8 and to exclusively use const
or let
instead for a while, to get the hang of it.
The massive difference between const
/let
and var
is the scope. The former obeys its scoping rules according to the nearest curly braces block, and the latter obeys its scoping rules to the enclosing function.
Any variable made with var
has an additional oddity in that it is "hoisted" -- and odd it is. If you haven't learned what hoisted is referring to but have been scripting happily along without knowing much about it, then that is a case in point to use const
/let
instead.
Whenever you make a variable that will not be reassigned to some value. It is important to understand that if the value itself is an object where assignments make sense (such as objects) you can still use assignment statements on those, just not on the variable immediately after the const
.
Whenever you make a variable that could be reassigned to some value. It doesn't have to be reassigned, but if there is no possibility of it, use const
instead.
There is an interesting pattern that can be used which I quite like. Here's the problem:
function getResponseFromUrl(url) {
try {
const response = UrlFetchApp.fetch(url);
} catch (e) {
Logger.log("Oh no, error");
}
return response; // fails
}
What's the problem? Well, const response
is defined inside the try {}
area, and is used again in the return
statement outside of it. You can't do that, because const
variables obey rules according to the immediate curly braces.
You could do this:
function getResponseFromUrl(url) {
let response;
try {
response = UrlFetchApp.fetch(url);
} catch (e) {
Logger.log("Oh no, error");
}
return response; // okay
}
That's just fine. But I much prefer this method:
function getResponseFromUrl(url) {
const response = (_ => {
return UrlFetchApp.fetch(url);
} catch (e) {
Logger.log("Oh no, error");
}();
return response; // okay
}
What the heck is going on there?
First of all one thing we have is a self-invoking function. You can do that:
(function log (text) {
Logger.log(text);
})("Hello World");
That calls the function log
immediately after being created, with the value "Hello World"
as text
. The arrow function equivalent is:
(text => {
Logger.log(text)
})('Hello World');
Which means we can get around the earlier const
problem by doing this:
const variable = (_ {
return 'value';
})();
Logger.log(variable);
The (_
is confusing at first, but it's actually the _
representing that there is no parameter, with a (
enclosing the function until )
, there it then calls itself with ()
.
Kinda cool.
I think of destructuring as bulk re-assignment. The syntax is hard to get used to, but it results in values being assigned. It's a bit weird, but here goes:
const bart = 'Bart Simpson';
const [firstName, lastName] = bart.split(' ');
firstName; // = 'Bart';
lastName; // = 'Simpson';
That's what it is for array assignments, but you can also do for object assignments. But when it comes to using objects, I think it more as a re-mapping of variables to values.
const {log: __log__} = Logger;
__log__('Hello World'); // same as Logger.log('Hello World');
What's happening here? It's assigning the variable log
to Logger.log
by evaluating it as an object reference. So clear right? I actually think that's kinda awful. Why not just do this?
const __log__ = Logger.log;
I like using __log__
for this, although you could just as easily use log
, but _
can be part of variable names and it's easy to find them again after you've finished debugging.
Destructuring is also useful for objects. The syntax follows the same pattern: You can make new variables and assign it values at the same time.
const obj = {greeting: 'Hello', name: 'World'};
const {greeting: hello, name: world} = obj;
Logger.log(hello + world); // "HelloWorld"
But why? Isn't using destructuring with arrays easier to read?
const obj = {greeting: 'Hello', name: 'World'};
const [hello, world] = [ obj.greeting, obj.name ];
Logger.log(hello + world); // "HelloWorld"
I think so anyway.
If an object doesn't have the key, you can assign default values too. This is best to understand in the context of a function definition as settings:
function DoSomethingUseful(settings) {
const {verbose=false: verbose, log=true: log} = settings
if (verbose && log) {
Logger.log('Hello World');
}
}
const obj = {verbose: true};
DoSomethingUseful(obj);
With destructuring, if the key and the variable name are identical, you don't have to type it twice. The above could be rewritten like so:
function DoSomethingUseful(settings) {
const {verbose=false, log=true} = settings
// verbose and log variables are defined!
if (verbose && log) {
Logger.log('Hello World');
}
}
const obj = {verbose: true};
DoSomethingUseful(obj);
If you think this is cool, check out named parameters, but we're better off spending some some with "spreading" things.
The ...
in code has actually more than one use but shares the same syntax. One thing it does is converts a variable that has a name, say args
into a list that is populated with assigned values, which I think of as "remaining":
const [one, two, ...remaining] = [1, 2, 3, 4, 5];
Logger.log(remaining); // [3, 4, 5]
Another sort of "remaining" usage, but this time for objects:
const {one, two, ...remaining} = {one: 1, two: 2, three: 3, four: 4};
Logger.log(remaining); // {three: 3, four: 4}
When applying values, that's when I think of it as "spreading."
function add (a, b) {
return a + b;
}
const args = [1, 2];
add(...args);
Full disclosure: I am a Pythonista first and foremost, which means I have very strongly influenced by Python and its ecosystem. This section is a good example of that. I think it is very grand to be able to give names to the parameters instead of by position. So much more readable. So much better.
So this is cool, but unsophisticated, and we should make it better:
function UnsophisticatedFunction(greeting="Hello", name="World") {
Logger.log(greeting + name);
}
UnsophisticatedFunction('Hello', 'World');
// ^--- outputs "Hello World"
UnsophisticatedFunction(greeting='Hello', name='World');
// ^--- outputs "Hello World"
UnsophisticatedFunction(name='World', greeting='Hello');
// ^--- outputs "World Hello" BOOOO
That last one is just dumb. But we can use destructuring to our advantage!
function SophisticatedFunction({greeting="Hello", name="World"}) {
Logger.log(greeting + name);
}
SophisticatedFunction({greeting='Hello', name='World'});
// ^--- outputs "Hello World"
SophisticatedFunction({name='World', greeting='Hello'});
// ^--- outputs "Hello World"
It works much better that way eh? Easier to read eh? What happens if we invoke with no parameters?
SophisticatedFunction();
// ^--- "TypeError: Cannot destructure property `greeting` of 'undefined' or 'null'"
That's happening because you're passing nothing as a parameter and it's trying to destructuring on nothing. No sweat, let's change the function declaration to avoid this:
function EvenMoreSophisticatedFunction({greeting="Hello", name="World"}={}) {
Logger.log(greeting + name);
}
The difference is in the ={}
part of the parameter list. If nothing is passed, it makes it an empty object {}
by default, which makes destructuring a-ok.
You can mix positional parameters and named parameters:
function Mixed(one='Hello', two='World', {greeting="Hello", name="World"}={}) {
Logger.log(one, two);
}
But that to me defeats the purpose of named parameters. Exception: If the name of the function gives away what the parameter is doing:
function doSomethingById(id, {greeting="Hello", noun="World"}={}) {
Logger.log(id);
}
You may wish a function that is using named parameters to require that a certain property be required to be passed. For example, in our hello world example, ensure that something is passed for greeting
and noun
. This is simple to do without named parameters:
function HelloWorld (greeting, noun) {
Logger.log(greeting + noun);
}
HelloWorld('Hello') // fails, which is what we want. yay
There is a way, and it borrows the idea of "interfaces" (a concept in other languages) to implement. It's kinda involved, though, which is why I wrote a library for it.
What if you want to accept some parameters with default values, but the function can also accept any remaining values, which is then an object?
You can! Do it like this:
function MostSophisticated({greeting="Hello", noun="World", ...kwargs}) {
Logger.log(kwargs); // object
}
No kwargs will not be an array, it will be an object with properties. Yes, because the ...
here just means "remaining."
Just don't. I'm not going to explain why it doesn't work very well, but it's very ugly and not worth it.
Get to know how to classes in JavaScript. Not only are they useful but they are powerful, and it's likely that others will start using them throughout their own code too.
Classes allow us to create things that are very expressive and useful. To see how useful they are, let's first just look at how we can play around with functions with them. Let's construct a nonsense "Hello World"
class.
class App {
constructor (value) {
this.value = value;
}
log () {
Logger.log(this.value);
}
get getter () {
return this.value;
}
set setter (value) {
this.value = value;
}
static createHello () {
return new App('Hello World');
}
}
You can think of a class as a kind of blueprint that declares how an object behaves, and there's a way to create "instances" of these objects built on the blueprint. Or you can think of a class as a function that has a bunch of functions contained inside, and holds state.
In the above example the only state that it holds is value
, making it not very useful. State is initialized by the constructor
function, which is invoked when you call the App
variable the same way you would a function:
const instance = App('a value, any value');
// ^--- executes this.value = value;
So that class is called like a function, which returns a thing with some functions defined, and state. We can interact with it according to the blueprint:
instance.log();
// ^--- executes Logger.log(this.value)
The blueprint says that "log is a function with no parameters, which can be invoked with .get
."
The next two items on the blueprints uses the getter/setter pattern, where get
and set
preceded the name of the function. The concept of a getter and setter is that you don't actually use them as a function, though.
instance.setter = 'Bonjour';
// ^--- executes this.value = 'Bonjour';
const value = instance.getter // value now equals "Bonjour"
Instead of using them as functions, you use assignment statements (or as part of an expression.)
The final piece of blueprint says that we have a function on the class itself. It says that we have a function called createHello
which we can invoke by using dot notation on the class variable App
.
const instance = App.createHello();
// ^--- executes return new App("Hello World");
In this way, we have created an instance of our little app which will output "Hello World"
when we use instance.log()
A function that is on the class itself might be called a "class method" in other languages; the others would be "instance methods." Having this distinction is useful. Instance methods change or retrieve state, or do calculations depending on the state. Whereas class methods are useful when you do something that doesn't depend on state.
What I really like is making instances with the class methods, in what might be called a "convenience method." In the example above, we use the class method to make an instance of the class that contains "Hello World"
in the state.
You can use this convenience method pattern to avoid having to use the new
keyword, which you have to do with classes. Sorta cool:
class App {
static new () {
return new App();
}
}
const app = App.new();
There are actually a few of these class methods in common use for built-in objects. Do these look familiar?
Object.create({});
// ^--- makes an duplicate object
Array.from('Hello World');
// ^--- makes an array of characters: ['H', 'e' ...]
I really like this as a design pattern:
class Utils {
power (base, exponent) {
return base ** exponent;
}
}
class App {
constructor () {
this.value = 1;
}
static get utils () {
return new Utils();
}
}
const app = new App();
const four = App.utils.power(2, 2); // returns 4
We'll take advantage of this pattern to solve a confusing change from Rhino to V8 below.
There is a major change in the global context. It's really confusing, but the change was a necessary one. In Rhino, you could use variables between files that were defined in the global context, and it was all good. I suppose it was done that way in order to make it easier to get started. In V8, that's all broken.
Why? It has to do with the files that are on the project and how they are parsed. The technical details are boring. What's really interesting is figuring out how to compensate without having to know all the niggly details.
In the end, though, we'll have a design pattern we can use to our advantage.
Let's keep it intuitive and learn by rule-making, and so here goes:
Any code that is executed before the endpoint is called should not refer to a function / variable in another file, unless it gets executed after the endpoint has been called
What the heck are you going on about. Let's break it down:
What's the endpoint? In the online editor, that's the function that you click play on. Or it's the onEdit
function that gets executed as a result of an edit in the spreadsheet. Or it's the function that is exposed via the library mechanism. Just whatever gets called is the endpoint.
So you click the run
button with Endpoint selected:
// before the endpoint is called
function Endpoint() {
// after the endpoint is called
}
// code here is also executed before endpoint is called
What about the case with more than one file? Let's see
// Code.gs:
const before = 'before endpoint';
function Endpoint () {
const after = 'after endpoint';
}
const afterButBefore = 'before endpoint too';
// Business.gs
const stillBefore = 'still before endpoint';
function NotTheEndpoint () {
const never = 'never gets here';
}
const alsoStillBefore = 'yes even here';
So, between those files, we can't really refer to variables or functions between the two (we'll see an exception in a minute).
Now we know what "the endpoint" is talking about, but also why it's the pivotal thing to think about for the next bit.
Because, depending on rules not worth knowing, it might work in some cases. But it has to do with the order in which the files are parsed. So it depends, but let's just nevermind all that. So, I'm being really literal in my rule-making. (Can you tell I'm a programmer?)
Glad you asked. It has to do with classes!
Check out the following code:
// App.gs
class App {
constructor () {}
get module () {
return new Module();
// ^--- does this work?
}
}
// Module.gs
class Module {
log (text) {
Logger.log(text);
}
}
// Code.gs
function Endpoint () {
const app = App();
app.module.log('Hello World');
}
This gets us to the "unless" part of the rule above. By all appearances, you might be thinking that the new Module()
won't work because it's in the "before the endpoint is called" area. Alas, it is not. That code gets executed well after the endpoint is called, in fact, and in this way you can organize your project accordingly.
This pattern, which is a form of composition, allows for the creation of files that does some aspec of the application logic, and also allow you the developer to keep various functions organized in files as you'd like them to be.
Great writeup. I think you're missing a
{
infunction getResponseFromUrl(url) {
right after the=>
.