0

I want to create a static property for a class named OuterClass. I want the value of this property to be an instance of another class, which is named InnerClass.

Here is the inner class. It has a property and a function.

// InnerClass.gs

function InnerClass() {
  this.myProperty = 42;
}

InnerClass.prototype.myFunction = function() {
  return 43;
};

And here is the outer class, which has only one static property.

// OuterClass.gs

function OuterClass() {
}
OuterClass.innerClass = new InnerClass();

However, when I try to call methods of the inner class, I get:

TypeError: Cannot find function myFunction in object [object Object].

// myScript.gs

function myScript() {

  console.log(OuterClass.innerClass.myProperty);   // 42.0
  console.log(OuterClass.innerClass.myFunction()); // TypeError: Cannot find function myFunction in object [object Object].

  var anotherInnerClassInstance = new InnerClass();
  console.log(anotherInnerClassInstance.myFunction()); // 43.0
}

Since calling the instance method on anotherInnerClassInstance works, I believe that I am having trouble with the static property OuterClass.innerClass because

  1. The constructor of InnerClass is hoisted, but InnerClass.prototype.myFunction is not.
  2. As OuterClass.innerClass is instantiated, it is instantiated with an incomplete instance, because InnerClass.prototype.myFunction was not hoisted, and is not yet attached to the instance created.

Is there a way to use a class instance as a static variable? Note that I have to use prototype-based classes because I'm really using Google Apps Script, which is based on an obsolete version of JavaScript.


For those who have been unable to replicate this problem, here is a link to the Google Sheet producing this error: https://docs.google.com/spreadsheets/d/1Gxylcrbg9rWHGmc68CgHFmZqJ20E5-pLgA6fmHkxhAA/edit?usp=sharing

Also, here's a direct link to the script project: https://script.google.com/d/1V0FYrgiB3a4rTtvd9StyDtWAZ13AqlPl4rpgauCWSKk46UbcdIj9nqJC/edit?usp=sharing

Cameron Hudson
  • 3,190
  • 1
  • 26
  • 38
  • 1
    Can I ask you about the method for replicating your issue of `TypeError: Cannot find function myFunction in object [object Object]` at `OuterClass.innerClass.myFunction()`? Because when `myScript()` is run after your script was copied and pasted to the script editor, no error occurs. And the log shows `42`, `43` and `43`. – Tanaike Dec 05 '19 at 04:59
  • 1
    your example works in chrome for me, is google apps scripts not built on v8? – Daniel Lizik Dec 05 '19 at 05:01
  • 1
    when you know hoisting is the issue, why not keep everything in order like you pasted here? – AZ_ Dec 05 '19 at 05:01
  • `myFunction ` is prototype function. So order does not matter. Its part of `__proto__` and which is shared across the all instance. So it will be attached to all instaces of your innerClass. Your example is working fine in any order – vipul patel Dec 05 '19 at 05:19
  • 1
    Can you please describe your file structure? I copy/pasted your code, changed console.log to Logger.log, ran it without any issues, and received the expected output. – Diego Dec 05 '19 at 05:29
  • @Tanaike and Daniel Lizik Since this code is for Google Apps Script, I will eventually expose functions for use in spreadsheets. Those functions will use these classes. For basic testing, the class functions can be run manually in the script editor (I'm using Google Sheets, specifically.) – Cameron Hudson Dec 05 '19 at 05:30
  • @AZ_ Good practice dictates that classes should be in their own files. – Cameron Hudson Dec 05 '19 at 05:31
  • @All since this may be a problem specific to Google Apps Script, I will change "JavaScript" in my post title to "Google Apps Script". – Cameron Hudson Dec 05 '19 at 05:31
  • @Diego In my current file structure, each code chunk shown is a separate GS file. I will clarify this in my post. – Cameron Hudson Dec 05 '19 at 05:32
  • @CameronHudson I separated it out into 3 files: Inner.gs, Outer.gs, and Code.gs. I still cannot replicate your issue. If you create a new project with just that code, do you get the same result? – Diego Dec 05 '19 at 05:35
  • Does this answer your question? [Which Edition of ECMA-262 Does Google Apps Script Support?](https://stackoverflow.com/questions/17252409/which-edition-of-ecma-262-does-google-apps-script-support) – Daniel Lizik Dec 05 '19 at 05:47
  • @DanielLizik Nope. I've added to my post a link to the Google Sheet producing the error. – Cameron Hudson Dec 09 '19 at 19:07

3 Answers3

2

The workaround that I ended up using was to use a getter to lazy-initialize the static property class instance. It's a bit more verbose, but this way, hoisting is not an issue.

// OuterClass.gs

function OuterClass() {
}

OuterClass.getInnerClass = function() {
  if (OuterClass.innerClass === undefined) {
    OuterClass.innerClass = new InnerClass();
  }
  return OuterClass.innerClass;
}
Cameron Hudson
  • 3,190
  • 1
  • 26
  • 38
2

Parsing order

Although I do not know exactly how script files were parsed internally in Rhino runtime (and your project suggests that you used it, and not the newer V8), in the migration guide it is mentioned that the order of files does not matter in Rhino, so let us assume the codebase is concatenated in one big chunk before being parsed.

It seems to be that the process is somewhat dependent on the file creation order. I managed to consistently reproduce your issue in Rhino by inspecting the order of calls using simple closures, take a look:

//bbbb.gs - was created first

function Parent() {}
Parent.child = (function () {
  console.log("child assigned");
  return new Child();
})();

//aaaa.gs

function Child() {
  console.log("child constructor");
}
Child.myMethod = (function () {
  console.log("child static method assigned");
  return function () {}
})();

Child.prototype.myMethod = (function () {
  console.log("child method assigned");
  return function () {}
})();

function testCP() {
  console.log( Parent.child.myMethod );  
}

As expected, the order of calls in the log is as follows when you run testCP:

logs of HOF calls

Now, if you switch the code in files, the logs look different (resulting in what you wanted):

logs of calls with files swapped

Alternative solution

The one you provided yourself with lazy loading (singleton pattern) should suffice. I would only suggest using dependency inversion to give yourself more flexibility and making InnerClass a hard dependency:

//bbbb.gs

function Parent() {}
Parent.setChild = function (child) {
  Parent.child = new child();
}

//aaaa.gs (omitted code is the same)

function testCP() {
  Parent.setChild(Child);
  
  console.log(Parent.child.myMethod); //function () {}
}

This time everything works as expected in any direction:

logs with DI

0

As other have mentioned in the question's comments your code works fine for me being a single script or threee different files. The only thing that I have done (as Diego already said) is changing console for Logger.

On executing the function myScript() I get the following logs in Apps Script (as expected):

[19-12-05 01:23:28:198 PST] 42.0

[19-12-05 01:23:28:199 PST] 43.0

[19-12-05 01:23:28:199 PST] 43.0

Also if you want to still code in pure JavaScript or TypeScript and later migrate to Apps Script I would suggest to use Clasp.

Community
  • 1
  • 1
Raserhin
  • 2,516
  • 1
  • 10
  • 14
  • I've added to my post a link to the Google Sheet producing the error. I am surprised that others have not been able to replicate the error. This might be a clue about the root cause. – Cameron Hudson Dec 09 '19 at 19:05