1

As of 2021, with V8 engine, I'm wondering if it's a good idea to use Global Variables in Google Apps Script? And if it is, how to use them? Is my way a good way (described below)?

Now I checked, of course, all other similar questions. But there are still some details I couldn't find:
Basically what I tried to do is to not repeat the code: I have multiple functions, and I'm storing the active spreadsheet and the current sheet like so:

const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getActiveSheet();

enter image description here which leads to

  1. repeating (1)
  2. wasting resources - instantiating spreadsheet and sheet (2)
  3. Increasing variables names inconsistency (ss / spreadsheet / spreadSheet - when copy/pasting from a snippet on the web) (3)

Right?

So I thought of using the Global Variables (GV) whenever I have common local variables in multiple functions.

However, since they’re will be unnecessarily allocated on every function call (there are also other functions that don't need the GVs), I tried to define them only when needed (only when there's function call that uses them) - by not using a defining keyword (var, const, let): enter image description here

According to this link, it seems to be a good approach (pattern 1).

Anyway, I'm wondering if there are any other considerations or downsides I'm not aware of? Is it really a good idea to go this road? Because so far, I didn’t see any snippet that implements this, and I saw a lot of GAS snippets.

One downside I'm aware of is the lack of autocompletion in the new GAS editor, for my GVs (since I didn't define them using 'var' or 'let' to set their scope Global on purpose).

Otherwise, I'm aware of PropertiesService and CacheService. However I'm willing to reuse my script (where I defined my GVs) as a library for my other scripts.
Plus, you can only store values as strings in PropertiesService and CacheService (and not SpreadsheetApp.getActiveSpreadsheet()) right? Not to mention that I don't need persistency after the script execution.
So I'm also hesitant to use them instead of GVs.

Rolm
  • 41
  • 1
  • 5
  • 1
    Consider [my answer](https://stackoverflow.com/questions/65150194/how-to-use-global-variables-while-avoiding-permission-errors) – TheMaster Nov 21 '21 at 16:43
  • The most common method of setting "global variables" is in fact by using Properties/CacheService. It's also important to note the fact that Apps Script's global variables are not the same as the ones used in other programming languages. From my understanding is that you actually want to use the script as a library - maybe I am missing something, but why not declare the variable once outside all of the functions? Moreover, have you taken a look at the approach suggested by TheMaster? @Rolm – ale13 Nov 22 '21 at 13:53
  • Thank you guys. @TheMaster, I saw your answer, and it is indeed very elegant. I like the idea of encapsulating the GV in an object and using a getter to access it. Now it's handy for 1 GV but **what if I had 10 of them?** (1) Would you have to repeat the 4 lines of code for each GV the same way like this: `get ss() { delete this.ss; return (this.ss = ...)); }` ? Instead, defining a new getter outside of the object with .defineProperty() doesn't seem much better. I saw your other reference 'Dynamically adding objects like sheets and range values using Lazy loading technique' (2) – Rolm Nov 22 '21 at 21:44
  • (2): [Here](https://stackoverflow.com/a/65134054/3696750), I see you're using `addSheetGettersToConfig_` to define new getters. But they're designed to return Ranges and their Values from the sheet (MESSAGE_BOARD,USERS,COMMENT_VOTES have the same datatype). What if you want to define GVs which don’t have the same datatype? Using the same code in this case, doesn’t seem to look elegant anymore. – Rolm Nov 22 '21 at 21:54
  • @ale13, yes I want to use my script as a library for other scripts. Defining variables outside of all functions is fine, except every time you call **any function**, they're used , even when not needed. – Rolm Nov 22 '21 at 21:59
  • 1
    @TheMaster, I just would like to clarify one thing: I like very much your solution, which I'm planning to use. But I'm asking my questions (1) and (2) just to get your feedback and see if there's still some room for improvement. – Rolm Nov 22 '21 at 22:28

1 Answers1

5
  • You can use the lazy loading technique in my answer
  • To make it dynamic and avoid repetition, You can use enclosing arrow functions(()=>{}) to avoid direct execution and use Object.defineProperty() to add a getter.
  • One of the significant advantage of this method is modular lazy loading. If a object isn't needed, it isn't loaded. If you have ss, sheet1, sheet2,rangeOfSheet1 and rangeOfSheet2 as global variables, if you access rangeOfSheet1, only it's dependencies are loaded, i.e, sheet1 and ss. The rest are untouched.
const g = {};//global object
const addGetter_ = (name, value, obj = g) => {
  Object.defineProperty(obj, name, {
    enumerable: true,
    configurable: true,
    get() {
      delete this[name];
      return (this[name] = value());
    },
  });
  return obj;
};

//MY GLOBAL VARIABLES in g
[
  ['ss', () => SpreadsheetApp.getActive()],
  ['MasterSheet', () => g.ss.getSheetByName('Sheet1')],
  ['MasterRangeColA1_5', () => g.MasterSheet.getRange('A1:A5')],
  ['MasterRangeColAValues', () => g.MasterRangeColA1_5.getValues()],
].forEach(([n, v]) => addGetter_(n, v));

const test = () => {
  console.info('start');
  console.log({ g });
  console.info('Accessing MasterSheet');
  console.log(g.MasterSheet);
  console.log({ g }); //note ss is loaded as well
  console.info('Accessing MasterRangeColAValues');
  console.log(g.MasterRangeColAValues);
  console.log({ g }); //note MasterRangeColA1_5 is loaded as well
};

Instead of a global object g, we can also use the global this, in which case, all variables directly become members of a global object:

const addGetter_ = (name, value, obj = this) => {
  Object.defineProperty(obj, name, {
    enumerable: true,
    configurable: true,
    get() {
      delete this[name];
      return (this[name] = value());
    },
  });
  return obj;
};
[
  ['ss', () => SpreadsheetApp.getActive()],
  ['MasterSheet', () => ss.getSheetByName('Sheet1')],
  ['MasterRangeColA1_5', () => MasterSheet.getRange('A1:A5')],
  ['MasterRangeColAValues', () => MasterRangeColA1_5.getValues()],
].forEach(([n, v]) => addGetter_(n, v));

const test = () => {
  console.info('start');
  console.log(this);
  console.info('Accessing MasterSheet');
  console.log(MasterSheet);
  console.log(this); //note ss is loaded as well
  console.info('Accessing MasterRangeColAValues');
  console.log(MasterRangeColAValues);
  console.log(this); //note MasterRangeColA1_5 is loaded as well
};
  • Advantage: You don't have to prefix variables with g. But, global space is polluted.
TheMaster
  • 45,448
  • 6
  • 62
  • 85
  • Sorry it took me some time to get back here. And thank you so much for taking the time to write a full, detailed answer with the code snippets! I tested and it works like a charm. Now about the code, there’s one point where I'm confused: where you delete the getter before you return it `delete this[name]; return (this[name] = value());`. Since it's deleted first, I thought it won't be returned.. Would you mind explaining this part please? – Rolm Nov 24 '21 at 14:57
  • @Rolm Since we access it first, even if it is deleted, it is deleted after the access. Think of it like this: ``var a = () => {a=()=>10; return 5}``. When `a` is accessed, it returns the function as a whole. So, even if `a` is rewritten to return 10, it returns 5 on first access execution(`a()`). On second access execution(`a()`), it returns 10. Try it out. – TheMaster Nov 24 '21 at 15:10
  • @Rolm Said otherwise, accessing a property returns a function. The function deletes the property on execution and returns a value. So `var b= {get c(){return 5}}` : accessing `c` on `b` first returns a function. Access ends. Function executes. Property deleted. Function returns to provide a actual value. In that order. – TheMaster Nov 24 '21 at 15:29
  • 2
    Oh wow, learned something new again. Got it now, but I never saw such way of coding before which is quite powerful. You define a function so that it behaves differently on the 1st execution compared to the next ones.. hum... Reminded me of the recursive function (the closest syntax to this one that I knew). Anyway, it's all good now. Thank you again. Really appreciate it. – Rolm Nov 24 '21 at 16:32