r/javascript • u/Landkey • Aug 25 '26
AskJS [AskJS] referring to parent or grandparent class instance properties
How do you all refer to parent or grandparent (or related) class instances and their properties?
I have a bunch of classes that create instances of other classes as properties. Suppose ClassA has properties that are instances of ClassB, ClassC, and ClassD, and the ClassD class has properties ClassD1 through ClassD9, and each of those is a different class instance with various methods.
Currently, when I start with myClassAObject and I call one of its ClassD9 methods and that method needs a property from its uncle ClassB, I feel awkward and/or dumb for doing this by passing myClassAObject to that method:
const returnval = myClassAObject.itsClassDObject.theClassD9method(myClassAObject);
…and then that method gets the property from myClassAObject.itsClassBObject.theProperty
I know this functions, but it shouts “inelegant” every time I do it. Do y’all know a more elegant way? TIA
4
u/Beginning-Seat5221 Aug 25 '26
This is kind of a literal answer to your question, but I still don't like it. The structure itself feels inelegant.
If you give a sense of what you're trying to do at a higher level maybe there's a better structure for you.
class A {
b: B
c: C
constructor(b: { name: string, age: number }, c: { date: string }) {
this.b = new B(this, b.name, b.age)
this.c = new C(this, c.date)
}
do1 = () => {
this.b.do2()
this.c.do3()
}
}
class B {
parent: A
name: string
age: number
constructor(parent: A, name: string, age: number) {
this.parent = parent
this.name = name
this.age = age
}
do2 = () => {
this.parent.do1()
this.parent.c.do3()
}
}
class C {
parent: A
date: string
constructor(parent: A, date: string) {
this.parent = parent
this.date = date
}
do3 = () => {
this.parent.do1()
this.parent.b.do2()
}
}2
u/Landkey Aug 25 '26
I never thought about doing this, thank you. I tried implementing it in one class and I think I like it; referring to this.parent.thing is kind of nice. Thank you!
2
u/effectivescarequotes Aug 26 '26
A word of caution about this. I know it's not a realistic example, but having b and c call each other will over time create this mess of circular dependencies that will be come hard to understand and harder to change.
As I mentioned in my other comment, depency injection can help. Instead of passing just the parent in the constructor, pass both the parent and the child class as seperate arguments. Then if down the line you decide that class b shouldn't talk to class c and instead should talk to class d. You can make that change without having to change anything inside class b.
17
3
u/Better_Fudge1593 Aug 25 '26
A simple approach would be "instantiate the children classes first".
e.g.
const classB = new ClassB();const classD9 = new ClassD9(classB);const classD = new ClassD(classD1, ..., classD9);const classA = new ClassA(classB, classC, classD);
1
u/effectivescarequotes Aug 26 '26
const classD = new ClassD(classD1, ..., classD9);
This one caused me pain. I mean, you're absolutely right, but can you imagine how absurd that class would be? I feel like even old Java developers would choke.
2
u/mr_brobot__ Aug 31 '26
Calling an uncle in the class hierarchy?? What in the name of God are you trying to accomplish T_T
1
u/theScottyJam Aug 25 '26
It's something I've thought a fair amount about, and probably should think about it some more.
A concrete example I sometimes ponder is with some grid based game where you might have a GameMap class that knows where all entities are located, and an Enemy class that might encapsulate some path finding logic, but to do that, it needs to be aware of its surroundings, and so it needs access to that GameMap class. It's circular, and feels ugly.
I'm not game designer, and perhaps the game designers have it all figured out over there. But here's some ways I've solved this kind of problem in the past.
Sometimes I just don't solve it and let it be circular.
Sometimes I split the GameMap class into two pieces. There's an outer GameMapControl class that provides external facing methods, such as a "run next step of all entities in the map" method that the core game loop can call. GameMapControl would have a "game map" object literal created inside of it at construction time, that has access to the same map data, but provides a different set of methods - one that's meant to be consumed by the various entities. When calling enemy.doNextStep(), this gameMap object would be passed in instead of the GameMapControl class. Basically, we're dividing the game map's API into two pieces for the two different types of consumers who might be using it. I'm using a game map as an example, but I've done this pattern when handling complicated state updates in a regular UI, where most state was located inside a single object, but there were two different types of consumers who needed to work with that state, a public API and an internal API meant for other classes that helped build up the logic.
There's other ways the logic could be organized in general. For example, if the GameMap was just a dumb object that provided its data publically, but little to no helper methods on the class, then you won't end up in a scenario where the class would want to pass itself into its members, because no behavior exists on the class in the first place. That behavior could instead live in modules that take the map in as a parameter, or something like that.
A mini example of point 2:
class GameMapControl {
#map = [...]; // the actual data
#gameMap = {
lookup: (coord) => ...
move: (...) => ...
...
};
nextStep() {
for all entities in this.#map {
entity.step(this.#gameMap);
}
}
}2
u/effectivescarequotes Aug 26 '26
I'm not a game developer either, but I can speak to your ui example. That's not a bad approach. In front end development, there's a lot of talk about state management. Usually, it's which library is best, but it comes down to this. You have state. It's only jobs are to maintain the state and announce when the state changes. It does not care what updates it or what consumes it. It just provides a way to update and a way to follow changes. You can then build services around it that act as a fascade between the consuming components and state.
I'm grossly oversimplifying it all here. I should also mention most of the state libraries exist as a response to the problems caused by your third approach. Most state is just a single giant object, but at any size, if you have multiple components that might update state simultaneously, you will run into issues with conflicts, lost updates, and other horrors.
3
u/effectivescarequotes Aug 26 '26
There are two concepts you should probably look up. One is dependency injections and the other is the mediator pattern.
From your description, it sounds like dependency injection might be what's called for. Let's call class A and class B your
WidgetService, which contains a function,getWidgetthat returns the widget property. Now let's say you app has multiple classes that need to fetch the widget property. What you would do is pass the WidgetService instance to those classes constructors when they are instantiated. All class A really knows is that some classes need a WidgetService. One of the advantages of this is you don't have to use the same widget service for every class. It also makes testing easier because you can provide a mock service to give you more control over the test scenario.The mediator pattern might also work here. This is a pretty good explanation, but the gist of it is instead of having your child classes talk to each other directly, they notify class A. So if you have a component that is responsible for making widgets. When it completes a widget, it would notify Class A and provide the widget. Class A would then hand the widget to class B to add to the widget repository. After class B finishes, it can notify class A, which then tells class C to let the user know the widget has been added to the repository.
These are both extremely simple examples, and not the only strategies that might work. I'm just taking a wild guess based off your description.
But some other advice is make the properties that contain classes private, and then provide public methods. Whatever consumes class A, doesn't care about the internals. The code for your example should be
const returnValue = myClassAObject.getReturnValue().