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.