1

I'm creating a class in Javascript (Note: I am actually using Google Apps Script, but this should be a generic Javascript question).

Inside of the class is an array of objects - specifically an array of another class I have declared. I want to ensure that the array contains ONLY instances of my class.

Here is the basics of what I have:

class subClassA {

  constructor(name) {

    this.name = name;
    this.mode = new Modes; //Creates instance of another custom class (not shown here)
    Object.seal(this); //Prevents adding new or removing any extensions to object without editing this class

  }
  
}

class subClassACollection {

  constructor() {

    this.array = [];
    Object.seal(this); //Prevents adding new or removing any extensions to object without editing this class
    return this.array; //Returns the array when used in formulas rather than the object

  }

}

class class1 {

  constructor(name) {

    this.name = name;
    this.mode = new Modes;
    this.collection = new subClassACollection;
    Object.seal(this); //Prevents adding new or removing any extensions to object without editing this class

  }

  addtoCollection(sca) {

    if (sca instanceof subClassA) {

      this.collection.push(subClassA);

    } else if (typeof sca == "string") {
    
      this.collection.push(new subClassA(sca));
    
    } else {

      //Incomplete, should generate an error or message to user here

    }

  }

}

What I want to do, is force the use of the '.addtoCollection' method to add another subClassA object to the collection/array to ensure that only instances of subClassA objects can be added and nothing else.

I was hoping by making the array it's own class, that would bury it a layer deep and not make it accessible. I thought using the return statement above would make it return the array for reading the values without making it modifiable from the outside. To my surprise, I can simply make an assignment like class1.collection = 'hi' which adds a string or any other variable or object type into the array/collection. I don't want to be able to do that! I want to ensure that the only thing that can be added to that array is of type subClassA - how do I accomplish that with a class?

NOTE: I have seen how to do this with a function - but I want to do this entirely within the class if possible, so these properties are inherent to the variables I'm creating, rather than assigning a function output to another variable. If it can be done with a method inside the class, that is fine - but I don't know how to keep the variables inside the method "alive" for the next call (similar to a static method in C).

Trashman
  • 1,424
  • 18
  • 27
  • `subClassACollection` is rather pointless if the constructor returns an array, not the `subClassACollection` instance. – Bergi Apr 05 '22 at 20:56
  • "*I have seen how to do this with a function*" - how would you do it there? – Bergi Apr 05 '22 at 20:57
  • @Bergi, I cannot do it here with this example. I'm saying if I did this an entirely different way and used a function INSTEAD of a class and used closures as described here: https://stackoverflow.com/questions/111102/how-do-javascript-closures-work – Trashman Apr 05 '22 at 21:14
  • @Bergi, as written, you're right it's a pretty pointless class. I basically created `subClassACollection` as an intermediate in hopes I could do something with it to achieve my end goal here. Apparently, if my environment supported private fields, I could have used it for just that as described by Ben West's answer. It doesn't appear that is the case, however. – Trashman Apr 05 '22 at 21:16
  • Ah, you mean closures. You still *can* create closures in the constructor of the class (closing over local variables of the constructor) and assign these closures as instance methods, but yes prototype methods cannot access these local variables. – Bergi Apr 05 '22 at 21:17

2 Answers2

2

In general JavaScript has no safety features like this. Having said that, we did recently get private fields, so if that's supported in your environment, you should be able to protect your collection array that way.

class Collection {
    #array;
    constructor() {
        this.#array = []
    }
    add (thing) {
        if (isOK(thing)) this.#array.push(thing)
    }
    get (index) {
        return this.#array[index]
    }
}
Ben West
  • 4,398
  • 1
  • 16
  • 16
  • 1
    …and even without the safety features, the hiding can simply be done by convention; using `this._array` and not documenting the array to be part of the public API is enough to meaningfully develop good software. – Bergi Apr 05 '22 at 21:01
  • Unfortunately, adding the line `#array;` in Apps Script gives me `Syntax error: ParseError: Unexpected token ILLEGAL line: 67 file: XXXxxxxxx.gs` it won't let me even save the file with that in there – Trashman Apr 05 '22 at 21:05
  • @Bergi - when trying `this._array` and editing in the Apps Script IDE, it does not hide the existence of `_array` - it even still suggests `_array` as an extension of my class while coding. – Trashman Apr 05 '22 at 21:09
  • @Trashman Yes, but every sensible developer [knows that `_array` is meant to be private](https://stackoverflow.com/q/4484424/1048572) and will not mess with the internals of your object. Why would they? If they break something, it's their fault; they have been warned. – Bergi Apr 05 '22 at 21:12
  • @Bergi unfortunately, I'm not counting on sensible developers here. – Trashman Apr 05 '22 at 21:18
  • 1
    @Trashman OK, I don't know the target audience of your apps script… Have a look at https://stackoverflow.com/q/22156326/1048572 for the alternatives then – Bergi Apr 05 '22 at 21:26
  • @Trashman If you're just talking about inexperienced (non-)developers, not outright unreasonable ones, you can also use a more egregious name, like `__UNSAFE_INTERNAL_ARRAY`, instead of the subtle underscore convention. – Bergi Apr 05 '22 at 21:29
  • 1
    @Bergi - the thread you linked to had an example that worked for me. I hadn't seen that method applied inside a class before and I didn't think it would work - that is, I thought the internal variable might not be kept static and I'd lose data. But it looks like it works. I mean, it's REALLY ugly and I'd prefer the stage 3 proposed solution (which is also kinda ugly, but not as bad), but that doesn't work yet for me so it looks like that's the best method right now. – Trashman Apr 05 '22 at 21:43
0

You want to create a getter/setter. collection should be a getter/setter.

mrall
  • 140
  • 4
  • 3
    The array would still be directly accessible in one form or another even with getters and setters wrapping it. They're merely a convenience. – Pellay Apr 05 '22 at 20:57
  • As you can see, I created a "setter" already (I used "add" rather than "set" in my method name, but it's essentially a setter). The issue is I can't make the "setter" the exclusive method of setting. It can still be set with a direct assignment, as @Pellay pointed out. I don't "need" a getter from my perspective, but if the implementation to force the "setter" means I have to also require a "getter" I would be fine with that. – Trashman Apr 05 '22 at 21:02
  • Yes indeed, the way JavaScript is, it is not really classes, but still prototype based objects at the core, so as Bergi said in the other comment, the general way is to just not document the core property. Getters and setters help with that, instead of addToCollection you would do a full getter/setter with a name like lastInCollection which internally would push to _collection when set, and _collection as the 'private' undocumented property. – mrall Apr 05 '22 at 21:24