写在前面
梳理一下:静态(static)、实例私有(#)与静态私有(static #)。围绕"是什么、为什么、怎么用、适用场景、继承行为、优缺点"等维度记录一下。
概念速览
- 静态(static) :定义在"类本身"上的属性/方法,不属于实例。访问方式
ClassName.member
。 - 实例私有(#) :每个实例独有的"真私有"字段/方法,只能在声明它的类的词法作用域内部访问,语法
this.#field
。 - 静态私有(static #) :定义在"类本身且私有"的字段/方法,只能在该类的词法作用域内访问,语法
ClassName.#field
(仅类体内可用)。
对照一下:
-
归属
- static:类级共享(构造函数对象)
- #:实例级独有
- static #:类级共享且私有
-
可见性
- static:公有(若不与 # 组合)
- #:真私有(外部与反射 API 均不可见)
- static #:真私有(对子类也不可见)
-
访问语法
- static:
Class.member()
/Class.prop
- #:类内
this.#field
- static #:类内
Class.#field
或this.#field
(静态上下文)
- static:
为什么会出现这些能力(动机?)
- 命名空间与组织性:将与实例无关的工具/常量放在类上,避免全局污染与冲突(static)。
- 封装与数据隐藏:提供语言层面"真私有"能力,杜绝外部访问与反射绕过(#、static #)。
- 性能与语义:实例状态与类级状态分离,减少不必要的实例开销,语义更清晰。
- 可维护性:清晰表达"共享状态 vs 实例状态"、"公有 API vs 内部实现"。
用法/示例
静态(static)
js
class MathX {
static PI = 3.14159
static clamp(v, min, max) {
return Math.min(Math.max(v, min), max)
}
}
MathX.PI
MathX.clamp(10, 0, 5) // 5
适合:工具函数、常量集合、静态工厂方法、单例入口(Class.instance()
)等。
更多示例:
js
// 1) 枚举常量与标签化
class HttpStatus {
static OK = 200
static NOT_FOUND = 404
static isOk(code) {
return code >= 200 && code < 300
}
}
// 2) 静态工厂 + 轻量缓存(公有 static + 私有实例)
class Color {
#hex
constructor(hex) {
this.#hex = hex
}
toString() {
return this.#hex
}
static cache = new Map()
static fromHex(hex) {
const key = hex.toLowerCase()
if (!this.cache.has(key)) {
this.cache.set(key, new Color(key))
}
return this.cache.get(key)
}
}
const red = Color.fromHex('#ff0000')
实例私有(#)
js
class Counter {
#count = 0
inc() {
this.#count++
}
value() {
return this.#count
}
}
const c = new Counter()
c.inc()
c.value() // 1
// c.#count // SyntaxError:类外不可访问
适合:实例内部状态、对外只暴露受控 API。
更多示例(带私有方法与输入校验):
js
class SafeQueue {
#items = []
#ensureNotEmpty() {
if (this.#items.length === 0) throw new Error('Queue is empty')
}
enqueue(item) {
this.#items.push(item)
}
dequeue() {
this.#ensureNotEmpty()
return this.#items.shift()
}
get size() {
return this.#items.length
}
}
静态私有(static #)
js
class Repo {
static #cache = new Map()
static get(id) {
return this.#cache.get(id)
}
static set(id, v) {
this.#cache.set(id, v)
}
static clear() {
this.#cache.clear()
}
}
Repo.set('u1', { name: 'Alice' })
Repo.get('u1')
// Repo.#cache // SyntaxError:类外不可访问
适合:类级共享但需要完全私有的数据/逻辑(缓存、注册表、密钥等)。
更多示例(对象池/复用):
js
class ConnectionPool {
static #free = []
static #busy = new Set()
static acquire() {
const conn = this.#free.pop() ?? { id: Math.random().toString(36).slice(2) }
this.#busy.add(conn)
return conn
}
static release(conn) {
if (this.#busy.delete(conn)) this.#free.push(conn)
}
static stats() {
return { free: this.#free.length, busy: this.#busy.size }
}
}
解决了什么问题?(落地场景)
- 工具/常量归档(static) :如
DateUtils.format()
、HTTPStatus.OK
。 - 单例服务(static + 私有) :
Logger.getInstance()
、Config.load()
;内部用static #
存储状态。 - 对象构建的语义化(静态工厂) :
User.fromJSON(data)
明确输入语义、可返回子类或缓存实例。 - 强化封装:实例私有状态不可被外部或子类"窥视与破坏"。
继承与多态行为
- 公有 static 可继承:
js
class A {
static x = 1
static who() {
return this.name
}
}
class B extends A {}
B.x // 1
B.who() // 'B',静态方法内 this 指向调用类,支持多态
- 覆盖(遮蔽)静态成员:
js
class A {
static x = 1
}
class B extends A {
static x = 2
}
A.x // 1; B.x // 2
- super 调用父类静态:
js
class A {
static greet() {
return 'A'
}
}
class B extends A {
static greet() {
return super.greet() + 'B'
}
}
- 私有名不继承 :
#
与static #
的私有名对外与对子类均不可见,子类若定义同名#x
也是不同的私有槽。
更多示例(多态静态工厂):
js
class Shape {
static create(data) {
switch (data.type) {
case 'circle':
return new Circle(data.r)
case 'rect':
return new Rect(data.w, data.h)
default:
throw new Error('unknown type')
}
}
}
class Circle extends Shape {
constructor(r) {
super()
this.r = r
}
}
class Rect extends Shape {
constructor(w, h) {
super()
this.w = w
this.h = h
}
}
实际项目中的应用范式
1) 静态工厂 + 实例封装
js
class User {
#name
#email
constructor(name, email) {
this.#name = name
this.#email = email
}
static fromJSON(j) {
return new User(j.name, j.email)
}
}
const u = User.fromJSON({ name: 'Alice', email: 'a@x.com' })
2) 单例服务(类级状态 + 私有)
js
class Config {
static #instance
static #data = new Map()
static instance() {
return (this.#instance ??= new Config())
}
static set(k, v) {
this.#data.set(k, v)
}
static get(k) {
return this.#data.get(k)
}
}
Config.set('env', 'prod')
Config.get('env')
3) 缓存/注册表(静态私有保护内部结构)
js
class RouterRegistry {
static #routes = new Map()
static register(name, handler) {
this.#routes.set(name, handler)
}
static resolve(name) {
return this.#routes.get(name)
}
}
4) 组合项目实践(与实际代码契合)
在复杂表单/查询组件中:
- 使用 实例私有
#
保存每个组件实例的临时状态(如输入缓冲、计时器句柄)。 - 使用 静态或静态私有 保存跨实例共享的缓存(如下拉备选项缓存/词典),并通过公有静态方法暴露受控访问。
5) Facade 门面 + 限流埋点
js
class HttpFacade {
static #lastAt = 0
static #minIntervalMs = 200
static async get(url) {
const now = Date.now()
if (now - this.#lastAt < this.#minIntervalMs) {
await new Promise((r) => setTimeout(r, this.#minIntervalMs - (now - this.#lastAt)))
}
this.#lastAt = Date.now()
// 这里可统一加鉴权头/埋点
return fetch(url).then((r) => r.json())
}
}
6) 词典缓存(跨组件共享)
ts
class DictCache {
static #map = new Map<string, any[]>()
static async get(name: string, loader: () => Promise<any[]>) {
if (!this.#map.has(name)) this.#map.set(name, await loader())
return this.#map.get(name)!
}
}
// 组件中:await DictCache.get('country', loadCountry)
优缺点
静态(static)
- 优点:组织清晰、节省实例内存、天然单例入口、易被 IDE/TS 分析。
- 缺点:过多静态全局化可能导致隐式依赖、测试隔离变难(需小心设计 API)。
实例私有(#)
- 优点:强封装,无法被外部或反射访问;语义明确,避免 API 泄漏。
- 缺点:调试与序列化不直观;与旧环境/装饰器生态的互操作性需注意。
静态私有(static #)
- 优点:类级共享同时保持完全私有;非常适合缓存、密钥、内部注册表。
- 缺点:对子类不可见,不能复用内部实现;重构时需要明确边界与对外 API。
得清楚
- 分清状态归属 :实例状态用
#
,跨实例共享用static
;需要隐藏则用static #
。 - 最小可见原则:默认私有,按需暴露公有静态方法作为网关。
- 控制全局性:对静态成员的修改要可追踪(记录、事件或只读常量)。
- 工厂优先 :涉及复杂创建流程时,优先静态工厂(
fromX
/of
/create
)。 - 测试友好:通过参数注入或静态"访问器"留出可替换点,避免单例硬耦合。
常见误区
- 认为 static 就能从实例上访问:错误,实例上访问不到静态成员。
- 以为
#
与私有约定(下划线)等价:不等价,#
是语法层面的强封装。 - 在子类中使用父类的
#
:不可行,私有名不向子类开放。 - 滥用静态全局状态:导致可维护性与测试隔离性变差。
进阶:class 高级用法与关键细节
静态初始化块(static { })
js
class Registry {
static #map
static {
// 初始化顺序:先字段初始值,再 static 块
this.#map = new Map()
// 可执行一次性注册/校验逻辑
}
static get(key) {
return this.#map.get(key)
}
}
适合一次性"装配"工作:常量校验、跨静态字段的依赖初始化、环境探测等。
更多示例(环境守卫与预热):
js
class Feature {
static #enabled = false
static {
const env = (typeof process !== 'undefined' && process.env && process.env.NODE_ENV) || 'prod'
this.#enabled = env !== 'test'
// 预热只在非测试环境执行
if (this.#enabled) {
// do warmup
}
}
static enabled() {
return this.#enabled
}
}
私有方法、访问器、以及静态私有方法
js
class Secure {
#secret = 'k'
#calc(x) {
return x + this.#secret
}
get value() {
return this.#calc('v')
}
static #hash(s) {
return `#${s}`
}
static encode(s) {
return this.#hash(s)
}
}
建议:
- 将实现细节下沉为私有方法;对外仅暴露极简、公有稳定 API。
- 静态私有方法适合封装类级算法(如序列号计算、缓存键生成)。
更多示例(键生成与签名):
js
class Signer {
static #salt = 's'
static #key(str) {
return `${str}:${this.#salt}`
}
static sign(str) {
return btoa(this.#key(str))
}
}
私有名"品牌检查"(brand check)
js
class A {
#x = 1
static isA(obj) {
try {
return #x in obj
} catch {
return false
}
}
}
说明:#x in obj
只能在声明该私有名的类词法作用域内使用,用于更"强"的类型守卫。
更多示例(跨模块安全检查):
js
class Token {
#v = 1
static isToken(x) {
try {
return #v in x
} catch {
return false
}
}
}
迭代协议与符号方法
js
class Range {
constructor(start, end) {
this.start = start
this.end = end
}
[Symbol.iterator]() {
let cur = this.start
const end = this.end
return {
next: () => (cur <= end ? { value: cur++, done: false } : { value: undefined, done: true })
}
}
get [Symbol.toStringTag]() {
return 'Range'
}
}
用途:让类可用于 for...of
、...
展开;自定义 Object.prototype.toString.call()
的标识。
TS 特性:抽象类、接口实现、构造函数重载
ts
abstract class RepoBase<T> {
abstract find(id: string): Promise<T | undefined>
}
interface User {
id: string
name: string
}
class UserRepo extends RepoBase<User> {
async find(id: string) {
/* ... */ return undefined
}
}
class Point {
// 重载签名(TS):
constructor(x: number, y: number)
constructor(p: { x: number; y: number })
constructor(a: number | { x: number; y: number }, b?: number) {
/* ... */
}
}
建议:用抽象类表达"可部分复用的骨架",用接口约束外部契约;构造重载表达输入形态。
更多示例(泛型仓库 + 静态工厂):
ts
interface Entity {
id: string
}
abstract class RepoBase2<T extends Entity> {
static fromEnv<TRepo extends RepoBase2<any>>(ctor: new () => TRepo): TRepo {
return new ctor()
}
abstract all(): Promise<T[]>
}
class MemoryRepo<T extends Entity> extends RepoBase2<T> {
#items: T[] = []
async all() {
return this.#items
}
}
const repo = RepoBase2.fromEnv(MemoryRepo)
mixin 与组合优先
- 避免深继承;将横切能力做成 mixin/组合函数,注入到具体类中。
- 在 Vue/React 等项目中,优先"组合函数 + 类"而非层层继承。
构造阶段的健壮性
- 使用
new.target
限制直接实例化抽象基类。 - 在构造函数中校验参数,保持类实例的"不变量"。
- 通过参数注入(依赖注入)提高可测试性。
与模块的取舍(静态类 vs 模块函数)
- 若功能仅是"无状态工具",可优先模块函数;需要封装状态/生命周期时再用类。
- 需要封装"私有共享状态"且暴露受控入口时,使用"静态 + 静态私有"更合适。
为什么要用 class 构造函数
- 表达不变量与初始化协议 :在
constructor
中集中做参数校验、默认值设置、依赖注入,保证对象一创建即有效。 - 类型友好(TS) :支持构造参数重载、参数属性(
constructor(public id: string)
)简化声明;有利于 IDE 推断与重构。 - 可测试/可替换:通过注入接口实现或 mock,解耦外部资源(HTTP、缓存、日志)。
- 生命周期挂钩 :构造时注册、销毁时清理(结合显式
dispose
)。 - 语义聚合:相关状态与行为集中在同一类实例上,避免散落函数造成的上下文丢失。
构造函数典型场景
- 实例有"身份/状态"的领域对象:
User
、Order
、Cart
、FormController
。 - 需要维护跨多次调用的"会话性状态":编辑器、下载器、数据拉取器(含取消/重试)。
- 需要多态的场景:策略对象、存储驱动、渲染后端(基类 + 多实现)。
例(数据拉取器,含取消/重试):
js
class Fetcher {
#controller = null
#retries = 0
#maxRetries = 2
async get(url) {
this.cancel()
this.#controller = new AbortController()
try {
const res = await fetch(url, { signal: this.#controller.signal })
return await res.json()
} catch (e) {
if (this.#retries++ < this.#maxRetries) return this.get(url)
throw e
} finally {
this.#controller = null
this.#retries = 0
}
}
cancel() {
this.#controller?.abort()
}
}
为什么要用"静态类"(仅静态成员的类)
说明:JS 无 static class
关键字,这里指"仅包含静态成员的类"。
- 集中入口:将相关工具/工厂/注册表置于同一命名空间,避免散乱导出。
- 私有共享状态 :用
static #
存储缓存/注册表/密钥,外部不可见、子类也不可见。 - 可替换点:在测试中可整体替换或 monkey-patch 公有静态方法。
- 跨实例共享:避免为每个实例重复创建相同数据/连接(谨慎使用)。
静态类典型场景
- 工具/常量聚合:
DateUtils
、MathX
、HTTP 状态常量。 - 注册表/缓存:
RouterRegistry
、ModelCache
(内部static #map
)。 - 工厂/单例:
Logger.getInstance()
、Config.instance()
;静态方法返回实例。 - 受控网关:对底层库/系统能力的门面(Facade),统一鉴权/限流/埋点。
例(Logger 单例 + 分级输出):
js
class Logger {
static #instance
static #level = 'info'
static getInstance() {
return (this.#instance ??= new Logger())
}
static setLevel(lv) {
this.#level = lv
}
info(...args) {
if (Logger.#level === 'info') console.log('[INFO]', ...args)
}
warn(...args) {
console.warn('[WARN]', ...args)
}
}
const log = Logger.getInstance()
log.info('ready')
构造函数 vs 静态类:应用场景对照
- 有状态对象(选 构造函数):需要多实例、持久状态、策略多态、与 UI/生命周期绑定。
- 无状态或类级状态(选 静态类):工具函数、常量、注册表、缓存、单例入口。
- 组合式设计:用"构造函数类"承载实例语义;用"静态类"提供工厂/缓存/门面。
- 演进路径:先模块函数 → 发现需要隐藏共享状态 → 升级为"静态类 + static #"。
兼容性与工程注意事项
- 转译与目标环境 :
#
/static #
在旧环境需经 Babel/TS 转译;注意输出目标(如 ES2019+)。 - 调试与序列化 :私有字段在 DevTools 不直观;通过显式
toJSON
控制序列化。 - 测试隔离 :修改静态状态后要在
afterEach
清理;或提供reset/clear
静态方法。 - 性能权衡:避免在构造函数做重活(I/O),延迟到方法调用或使用静态缓存。
- 最小暴露:默认私有/内部,仅暴露必要的公有 API 与类型。
测试隔离示例:
js
class CacheX {
static #m = new Map()
static set(k, v) {
this.#m.set(k, v)
}
static get(k) {
return this.#m.get(k)
}
static clear() {
this.#m.clear()
}
}
// test
afterEach(() => CacheX.clear())
总结
- static:类级共享,适合工具/常量/工厂/单例入口。
- #(实例私有):实例内部状态的强封装。
- static #(静态私有):类级共享但严格私有的实现细节与数据。
选择关键:先判断"状态归属"(实例 vs 类),再决定"可见性"(公有 vs 私有),最后以最小暴露面提供稳定 API。