HOWTO · JavaScript

JavaScript 中的静态变量

本教程介绍了 JavaScript 中的静态变量。

本页内容

JavaScript 可以通过公开的类静态字段、函数对象上的属性,或由闭包保留的局部变量,在多次调用之间保存值。这些方式都能保留状态,但状态的归属位置和访问方式各不相同。若要让整个类共享状态,可以使用类字段;若只需为某个函数保存简单的公开状态,可以使用函数属性;若希望调用方使用该值,却不能直接读取或更改其绑定,则可以使用闭包。

JavaScript 中的静态变量意味着什么

静态变量是关联到函数或类、而非某一次特定调用的状态。JavaScript 没有一种适用于所有函数的通用静态变量声明,而是提供了几种实现方式。static 关键字可在类本身上声明字段或方法。赋给函数的属性也归该函数对象所有。闭包则能让内部函数在外部函数返回后,继续访问一个局部绑定。

这些概念彼此相关,但相应机制并非在所有方面都可以互换。类字段和函数属性可以通过所属的类对象或函数对象访问。闭包可以将绑定保持为私有状态,并且只公开你选择提供的操作。

使用类静态字段保存类级状态

公开的类静态字段属于类,因此所有实例都通过类名访问同一个字段;该字段不会复制到每个实例中。这种语法要求运行环境支持公开的类静态字段。如果运行环境不支持,可以改用下文的函数属性或闭包方式。

运行此示例:

本节中的代码以及显示的标签或输出仍为英文。

class VisitCounter {
  static count = 0;

  static recordVisit() {
    return ++VisitCounter.count;
  }
}

console.log(VisitCounter.recordVisit());
console.log(VisitCounter.recordVisit());
console.log(VisitCounter.count);
1
2
2

这两次调用都会更新同一个 VisitCounter.count。由于该字段是公开的,其他代码也可以通过 VisitCounter.count 读取或赋值;如果希望由类本身控制访问,请使用私有静态字段(名称以 # 开头)。静态成员应通过类访问,而不是通过 new VisitCounter() 这样的实例访问。

为函数添加属性

函数是对象,因此命名函数可以拥有一个跨调用保留值的属性。这种方式简洁,适合为某个函数关联计数器或缓存。该属性是公开的:其他代码可以检查或更改 nextId.counter,因此这种方式不提供隐私保护。

本节中的代码以及显示的标签或输出仍为英文。

function nextId() {
  nextId.counter = (nextId.counter ?? 0) + 1;
  return nextId.counter;
}

console.log(nextId());
console.log(nextId());
console.log(nextId.counter);
1
2
2

构造函数也适用同样的思路。Widget.count 这样的属性属于构造函数,而 widget.name 属于某个实例。每个实例都不会各自获得一份 Widget.count 副本;代码通过 Widget 访问这个共享属性。从状态的归属来看,这类似于类级状态,但 JavaScript 函数属性并不具备某些类系统提供的全部访问控制或类型保证。

使用闭包保存私有状态

闭包使内部函数能够在外部函数执行完毕后,继续使用其中的局部绑定。IIFE(立即调用函数表达式)会立即创建外层作用域,并返回一个能够更新所保留值的操作。由于该绑定本身没有放在公开对象上,外部代码无法直接读取或赋值。

本节中的代码以及显示的标签或输出仍为英文。

const counter = (function () {
  let count = 0;
  return function () {
    return ++count;
  };
})();

console.log(counter());
console.log(counter());
1
2

这里,counter 闭包引用了 count;每次调用 counter() 都会递增同一个保留值,因此调用会依次输出 1 和 2。该值是私有的,而 counter 是公开接口。若要创建彼此独立的状态,请分别调用一个创建并返回新闭包的工厂函数;调用这个返回的 counter 只会递增它已有的状态。如果调用方需要重置或查看计数值,可以返回一个带有明确方法的小对象,而不是公开该绑定本身。

为什么应避免使用 arguments.callee

arguments.callee 通过函数的 arguments 对象引用当前正在执行的函数。早期的一些写法会用它给该函数添加属性,效果类似于给命名函数添加属性。这是一种遗留技巧,在严格模式下被禁止,因此应用严格模式时,使用它的代码可能会失败;ECMAScript 模块也包括在内。使用命名函数能让这种关联更加清晰,而闭包无需依赖这个受限属性,就能保存私有状态。新代码应优先采用上文介绍的受支持方式,不要围绕 arguments.callee 构建。