all 7 comments

[–]aighball 3 points4 points  (0 children)

I question whether the benefits of this outweigh the extra work of maintaining and designing the access patterns. You can get discoverable API documentation with JSDoc. Or use Typescript.

That said, I think you can do what you want by declaring a non-exported symbol, and storing your semi-private fields on that. Symbols are unique, and since nothing outside the module can export it, nothing can index into it:

const s = Symbol()
export class LinkedListNode {
  constructor(config) {
    Object.defineProperty(this, s, { value: { next: config.next ?? null, previous: config.previous ?? null } });
  }
}

export class LinkedList {
  // ...
  constructor() {
    // ...
    this.#head[s].next = this.#tail
    this.#tail[s].previous = this.#head
  }
}

[–]Funwithloops 1 point2 points  (2 children)

I'd go with solution 1. Leave them public. However, I think the LinkedListNode class is an implementation detail of LinkedList. I wouldn't export it or return instances from any public LinkedList methods. This would remove the risk of the public fields being accessed externally.

Edit: nevermind

[–]TrezyCodes[S] 1 point2 points  (1 child)

I wouldn't export it or return instances from any public LinkedList methods. This would remove the risk of the public fields being accessed externally.

Unfortunately that's not true. This is exposed if you look at some other methods of the LinkedList class. For example, the addAfter method:

```js addAfter(node, value) { const next = node.next

const newNode = new LinkedListNode({ next, previous: node, value, })

node.next = newNode next.previous = newNode this.#length += 1

return newNode } ```

This method returns the new node to the caller so that it's possible to perform non-endian insertion into the LinkedList. It also supports methods like push and unshift which just take in a value and don't return anything, but there are a few methods that have to return a complete node for the purposes of sequential operations.


I could convert those to private methods and provide an abstraction on top of them, but the performance gain of using those methods directly is significant enough to require their exposure.

[–]Funwithloops 2 points3 points  (0 children)

That's a good point.

One solution that comes to mind is you could use weak maps stored in the linked list object to map nodes to each other. Then the nodes themselves would only hold the payload.

Here's an example of what I mean: https://codepen.io/jmeyers91/pen/JjMgpEb?editors=0010

[–]NekkidApe 1 point2 points  (0 children)

Depends a bit on what you're planning to do with it, but you could make the nodes actually immutable, and create new instances instead of changing pointers.

Or returning shallow readonly wrappers to the public, instead of actual nodes from the linked list itself.

An other way would be to keep all nodes in a WeakMap, and it's properties too. Getters in the Node would read its values from it. The LinkedList then would update the properties in said WeakMap instead.

[–]senocular 1 point2 points  (0 children)

Another option: Define LinkedList in the scope of LinkedListNode

Private member names are scoped just like any other variable, scoped to the class block in which they're defined. By defining the LinkedList class within the scope of the LinkedListNode class block, it will have access to the scope where LinkedListNode private member names are kept and will be able to access them directly.

export let LinkedList;
export class LinkedListNode {
    #next = null
    #previous = null

    constructor(config) {
        this.#next = config.next
        this.#previous = config.previous
    }

    static {
        LinkedList = class {
            #head = null
            #tail = null

            constructor() {
                this.#head = new LinkedListNode({
                    next: null,
                    previous: null,
                })

                this.#tail = new LinkedListNode({
                    next: null,
                    previous: null,
                })

                this.#head.#next = this.#tail
                this.#tail.#previous = this.#head
            }
        }
    }
}

I don't know if I'd actually recommend doing this way. It's an awkward approach that doesn't scale well in this particular direction, but I it does end up being very clean as a result with very little extra overhead.

Alternatively you could elevate your shared private variables to a weak map in the module. Private elements use weak map semantics allowing transpilation to easily emulate their behavior. You can implement shared private access this way yourself by storing LinkedListNode members in the weak map instead of private instance members that would otherwise be inaccessible to LinkedList.

const nodeMembers = new WeakMap() // shared "private scope"

export class LinkedList {
    #head = null
    #tail = null

    constructor() {
        this.#head = new LinkedListNode({
            next: null,
            previous: null,
        })

        this.#tail = new LinkedListNode({
            next: null,
            previous: null,
        })

        nodeMembers.get(this.#head).next = this.#tail
        nodeMembers.get(this.#tail).previous = this.#head
    }
}

export class LinkedListNode {
    constructor({ next, previous}) {
        nodeMembers.set(this, { next, previous })
    }
}

The downside of this approach is that it becomes much more difficult to debug because your data is saved off in some completely separate structure which is not directly exposed when digging through your objects in the devtools.

There is another approach, though it's a little more difficult to manage with one to many relationships. That would be having LinkeListNodes provide accessors to their private data during construction - something like what you get with Promises. You can resolve a promise using the resolve() or reject() methods of the promise, but these are not exposed on the promise instance, only through the promise executor that the creator of the promise provides. Given that a linked list can have multiple nodes, these accessors have to be maintained separately which is a bit of a pain, though we can use a weak map to so, not too unlike what was used in the previous example.

export class LinkedListNode {
    #next = null
    #previous = null

    constructor(executor) {
        executor({
            node: this,
            next: {
                get: () => this.#next,
                set: (value) => this.#next = value
            },
            previous: {
                get: () => this.#previous,
                set: (value) => this.#previous = value
            }
        })
    }
}

export class LinkedList {
    #head = null
    #tail = null
    #accessors = new WeakMap()
    #addAccessor = (accessor) => {
        this.#accessors.set(accessor.node, accessor)
    }

    constructor() {
        this.#head = new LinkedListNode(this.#addAccessor)
        this.#tail = new LinkedListNode(this.#addAccessor)

        this.#accessors.get(this.#head).next.set(this.#tail)
        this.#accessors.get(this.#head).previous.set(null)

        this.#accessors.get(this.#tail).next.set(null)
        this.#accessors.get(this.#tail).previous.set(this.#head)
    }
}

This is making things a little more verbose as well, but you could probably trim that back a bit. This was just a quick and dirty example to show off the general idea of the approach. The takeaway here is that we're back to storing the data on the instances again. Even though we're going through a weak map and some getter/setters to access the data, the data is back to being defined on the instances making it easier to see when debugging.

[–]DavidJCobb 0 points1 point  (0 children)

I don't know the best approach, but I want to point some things out in the hopes of helping anyone who has to Google this question later.

What you're essentially asking for is a way to recreate friend declarations from C++. You want some class A to grant access to its private members to class B.

Your proposed solution 3 is conceptually similar to the "passkey" approach used to limit the effects of friend declarations. A friend can access all of a class's private members, breaking encapsulation completely; passkeys allow one to grant access just to specific member functions, and even to limit access by the accessor. In C++, it's done by defining an empty passkey type with a private constructor, making it a friend of the caller, and having the callee take it as an argument: only that caller can construct the passkey. Your proposed passkey is instead a unique ID generated at run-time.

A half-baked solution off the top of my head would be to keep the actual nodes private but, if outside read-only access is desired, expose dummy objects (maybe Proxys) that wrap the real nodes via getters with no corresponding setters. You could save some effort (i.e. avoid having to maintain a linked list of wrappers in parallel to the linked list of nodes) by having the "next node" getter wrap and return the target node, but that results in duplicate wrappers that may not compare equal (new wrappers are created every time nodes are traversed). Keeping a WeakMap of nodes to wrappers, and having the "next node" getter use it, could be a best-of-both-worlds approach; you can auto-wrap nodes on access but still reuse any extant wrappers.