Search code examples
google-apps-scriptglobal-variablesdry

Avoid Repeating: Use Global Variables in Google Apps Script or Not?


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.


Solution

    • 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.