Private class fields are private, unlike an underscore

A leading underscore is a convention that anything can ignore; a hash-prefixed field is a syntax error to access from outside.

class Cache {
  #entries = new Map()

  static #instances = 0

  #evict() { /* ... */ }

  has(k) { return Cache.#exists(this, k) }
  static #exists(c, k) { return c.#entries.has(k) }
}

new Cache().#entries      // SyntaxError, at parse time

// and the brand check, which is the idiomatic instanceof
static isCache(o) { return #entries in o }

The #field in obj brand check is the underused part and is a more reliable test than instanceof across realms or after a class is extended. Genuine privacy also means a debugger and a serialiser cannot see the field, which is occasionally what you want and is a real change from the underscore convention.