静态成员属于类,而不属于实例
你在类上写的大多数方法,操作的都是某个具体实例:某个 user、某个 circle。静态成员不一样,它们属于类本身。你通过类名调用它们,而不是通过实例。
double 存在于 MathUtils 上,而不在它的实例上。创建实例也没用:m.double 是 undefined。这和普通方法正好相反,普通方法存在于实例上(严格来说是在原型上),对类本身不可见。
思维模型是:class 关键字会创建两样东西,一组实例方法(供 new MathUtils() 使用)和一组静态方法(供 MathUtils 直接使用)。static 关键字决定一个成员归入哪一组。
经典用法:工厂方法
写静态方法最常见的理由是提供另一种构造方式。真正的 constructor 接收什么参数就是什么参数,但你常常还需要用其他方式创建对象:从 JSON、从数据库的一行数据、从一个 URL。
fromJSON 不是在使用一个用户,而是在生成一个用户。这正是适合静态方法的场景。另一种做法是写一个独立的 parseUser 函数,但把它放在类上,可以把相关行为归到一起,调用处的意图也一目了然。
静态属性
你也可以把数据挂在类本身上:
Circle.PI 被所有圆共享:它是类上的一个常量,不会被复制到每个实例中。在实例方法内部,你通过类名(Circle.PI)引用它,而不是通过 this。
静态属性很适合存放配置、在实例间共享的缓存、计数器以及类级别的常量。
静态方法中的 this 是类
在普通方法中,this 是实例。在静态方法中,this 是类:
increment 中的 this.count 就是 Counter.count。一开始可能觉得有点怪,但正是这一点让静态方法的继承得以工作:this 指向你调用方法的那个类,而不是定义方法的那个类。
静态方法与继承
静态方法会被子类继承。又因为 this 指向调用方法的那个类,工厂方法会自动生成正确的子类:
Animal.create 使用了 new this(name)。当你调用 Dog.create("Rex") 时,this 是 Dog,所以 new this(name) 创建的是一个 Dog。如果这里写成 new Animal(name),就永远只会得到 Animal,这个模式也就失效了。这正是静态方法中 this 指向类的主要原因。
静态与实例:一个对照示例
下面是同一类逻辑分别用两种风格来写:
两者做的是同样的计算。实例版本从 this.celsius 读取数据,静态版本则把输入作为参数接收。如果操作是“这个对象要做的事”,选实例方法;如果是“这个类知道怎么根据输入计算的事”,选静态方法。
用静态块做初始化
有时初始化一个静态属性需要的不止一个表达式:一个循环、一个条件判断、多个相互依赖的值。静态块就是为此准备的:
static { ... } 块在类定义时运行一次。在块内部,this 是类。多步初始化就用它;如果只是一次赋值,普通的静态字段更清晰。
私有静态成员
静态字段也可以是私有的,同样使用 # 前缀。它们只能在类内部访问:
#nextId 被封装在类内部。外部代码可以调用 IdGenerator.next(),但无法访问或重置这个计数器。私有字段很快会有专门的一页,但值得先知道 static 和 # 可以组合使用。
什么时候不该用 static
静态方法是归类辅助函数的好办法,但这不是把每个工具函数都变成类的理由。如果你的文件里全是互相独立的函数,就直接导出这些函数,别为了加个命名空间而把它们包进一个全是静态方法的类里。模块本身就能做这件事,而且做得更干净。
在这些情况下使用 static:
- 这个函数确实属于某个类(该类型的工厂、转换器、验证器)。
- 你需要一份在该类所有实例之间共享的状态。
- 子类可能想要覆盖或继承这个行为。
否则,普通函数是更简单的工具。
下一篇:私有字段
你刚才简单见过 #nextId,这就是 JavaScript 的私有字段语法。它适用于实例成员和静态成员,是在类中隐藏实现细节的现代方式。这就是下一页的内容。
常见问题
JavaScript 中的静态方法是什么?
静态方法属于类本身,而不属于类的实例。用 static 关键字声明,并在类上调用:MyClass.doThing()。实例无法通过 this.doThing() 访问它,只有类本身可以。
什么时候该用静态方法而不是实例方法?
当一个函数与类相关,但不需要读取或修改某个具体实例时,就用静态方法。常见场景有:User.fromJSON(...) 这样的工厂方法,Math.max 这样的工具函数,以及把类当作命名空间存放的常量。如果方法需要 this 指向一个实例,那它就是实例方法。
静态方法能访问实例属性吗?
不能直接访问。在静态方法内部,this 指向的是类而不是实例,所以 this.name 读取的是静态属性,而不是实例字段。如果需要实例数据,就把实例作为参数传进去:static summarize(user) { return user.name; }。
JavaScript 中的静态方法会被继承吗?
会。子类继承父类时,父类的静态方法在子类上同样可用。在静态方法内部,this 指向你调用它的那个类,正因如此,静态工厂方法才能在各个子类中正确工作。