五年前[^1]我写过一篇文章梳理 ES6 class 和 ES5 构造函数的转换。在那篇文章中有一个话题没有展开,那就是 class 当中静态成员的一些行为,尤其是静态函数的一些行为。这篇文章来对静态成员的一些行为进行探究。
static
成员及继承
我们已经知道,如果我们写了如下的一个 class:
class BaseClass {
static staticFunc1() {}
}
这种写法等价于在 ES5 中的写法:
function BaseClass() {}
BaseClass.staticFunc1 = function() {};
容易验证:
class BaseClass {
static staticFunc1() {}
}
console.log(typeof BaseClass); // 'function'
console.log(Object.hasOwn(BaseClass, 'staticFunc1')); // true.
const baseClass = new BaseClass();
console.log(baseClass.constructor === BaseClass); // true
console.log(Object.hasOwn(baseClass, 'staticFunc1')); // false. Not a instance member
如果我们使用继承,那么 static
成员也会通过原型链得到继承:
class DerivedClass extends BaseClass {
static staticFunc2() {}
}
等价于:
function DerivedClass() {}
// ...此处省略对实例成员的继承...
// 对静态成员的继承
Object.setPrototypeOf(DerivedClass, BaseClass);
DerivedClass.staticFunc2 = function() {};
此时可以发现,对于成员 staticFunc1()
,如果我们想要在 staticFunc2()
当中调用,有三种方式:
BaseClass.staticFunc1()
;DerivedClass.staticFunc1()
;this.staticFunc1()
。
这三种有什么区别?
static
方法中的 this
首先,我们要搞清楚在 static
方法当中,this
的指向。
我们已经知道,
对于典型的函数,
this
的值是函数被访问的对象。换句话说,如果函数调用的形式是obj.f()
,那么this
就指向obj
。[^ref1]
因此,对于上述三种方式,this
的值分别是:
BaseClass
;DerivedClass
;staticFunc2()
此时的this
。
注意,static
方法(方式 3)是不能在 DerivedClass
实例上调用的,因为 DerivedClass
实例上不存在成员 staticFunc1
:
class DerivedClass extends BaseClass {
// ...
instanceFunc() {
this.staticFunc1(); // TypeError: this.staticFunc1 is not a function
}
}
容易注意到,对于 this
的值,方式 1 和 2 是确定的,但是 3 是动态的。这一特性可以用于实现一些“多态”功能[^2]。
static
方法的“多态”
我们有时需要在基类当中访问子类的 static
成员。例如我们编写一个前端框架组件类,基类负责生命周期和渲染,而子类负责提供 HTML 和 CSS。典型地,CSS 一般通过 static
成员提供来节省内存[^3]:
class CustomElement {
// ...
// Just for overriding
static get styles() {return ``;}
// Called by other lift cycle members
private renderDOM() {
// How to get MyCoolElement.styles?
}
// ...
}
class MyCoolElement extends CustomElement {
// ...
static get styles() {
return `
p { text-align: both;}
...
`;
}
// ...
}
我们不能在 CustomElement
当中使用 MyCoolElement.styles
,因为作为基类不可能知道子类。这里的技巧在于,我们可以利用 this
的动态特性,以及 constructor
属性。
记得我们在上面提到的:
console.log(baseClass.constructor === BaseClass); // true
那么我们在 renderDOM()
当中得到的 this
是谁?是 MyCoolElement
。如果你不明白为什么,参考我之前的文章。简单来说,JavaScript 的继承不会构造两个实例,只会构造一个实例,因此 this
总是指向被构造的实例。
那么剩下的问题就简单了。通过 this.constructor
,我们就可以在运行时从基类当中得到子类的静态成员:
class CustomElement {
// ...
// Called by other lift cycle members
private renderDOM() {
// this.constructor is MyCoolElement at runtime
const styles = this.constructor.styles; // 'p { text-align: both;}...'
}
// ...
}
注释
- [^1]: 什么叫丧心病狂地摸鱼啊(后仰)。
- [^2]: 尽管在 JavaScript 并不存在类似于 C++、Java 一样的多态,但是行为上是相似的,都是到运行时再决定具体对象。
- [^3]: Lit 就是这么做的。
参考文献
- [^ref1]: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/this