先记住这个答案
会被继承。class Dog extends Animal 做了两件事:一是让 Dog.prototype 的原型指向 Animal.prototype,这是实例方法能继承的原因;二是让 Dog 这个构造函数本身的原型指向 Animal,这就是静态方法能继承的原因。所以 Dog.create() 在 Dog 上找不到,会顺着原型链找到 Animal.create()。子类想在自己的静态方法里调父类版本,用 super.create()。静态方法里的 this 指向实际调用它的类,不一定是定义它的类。
extends会建立子类构造器原型链- 静态方法里的
this是调用它的那个类 - 子类静态方法可用
super调父类版本 - 实例不能直接调用静态方法
一个工厂方法看清继承、super 和 this
静态方法最常见的用途是工厂方法。下面父类定义了 create,子类重写后又调了父类版本。
class Animal {
static count = 0
static create(name) {
this.count++ // this 是调用它的那个类
return new this(name) // 用 this 创建,子类调用就创建子类实例
}
constructor(name) { this.name = name }
}
class Dog extends Animal {
static create(name) {
const dog = super.create(name) // 调父类的静态方法,this 仍是 Dog
dog.kind = 'dog'
return dog
}
}
const d = Dog.create('Lucky')
console.log(d instanceof Dog, d.kind)
console.log(Object.getPrototypeOf(Dog) === Animal)
console.log(Animal.count, Dog.count, Object.hasOwn(Dog, 'count'))
console.log(typeof d.create)查看输出与解释
true dog
true
0 1 true
undefinedDog.create 里调 super.create(name),执行的是 Animal.create 的代码,但 this 仍然是 Dog,所以 new this(name) 创建的是 Dog 实例。父类写工厂方法时用 new this() 而不是 new Animal(),子类就能直接复用。
第三行是个陷阱。this.count++ 先读 Dog.count,Dog 自己没有,顺着原型链读到 Animal.count 的 0,然后赋值 Dog.count = 1。赋值不会沿原型链去改父类,而是在 Dog 身上新建了一个 count。所以父类还是 0,子类变成了 1,而且是子类自己的属性。想让所有子类共用一个计数,要明确写 Animal.count++。
最后一行说明实例拿不到静态方法。实例的原型链是 Dog.prototype、Animal.prototype、Object.prototype,静态方法在 Dog 和 Animal 本身上,不在这条链里。
用 ES5 的写法理解它
在 class 之前,静态方法就是直接挂在构造函数上的属性,比如 Animal.create = function () {}。用 ES5 实现继承时,大多数人只写了 Dog.prototype = Object.create(Animal.prototype),静态方法是继承不过来的,需要再补一句 Object.setPrototypeOf(Dog, Animal)。
extends 相当于这两句都帮你写了。这也是为什么 Babel 编译 class 继承时,生成的辅助函数里会有一步设置子类构造函数原型的操作。
容易答错的地方
- 以为静态方法里的
this固定是定义它的类 this看调用方式。Dog.create()调用时this是Dog。如果把方法取出来单独调用,比如const fn = Dog.create; fn(),this就是undefined,class 内部是严格模式,new this()会直接报错。- 以为子类修改静态字段会影响父类
- 正好相反。子类赋值会在自己身上创建同名属性,父类不受影响。只有子类还没有自己的同名属性时,读取才会拿到父类的值。
- 以为私有静态字段也能被子类继承
static #total只存在于Animal身上。父类静态方法里写this.#total,如果通过Dog.method()调用,this是Dog,而Dog没有这个私有字段,会抛TypeError。这种情况要写成Animal.#total。
面试官还会怎么问?
实例方法里怎么调静态方法?
用 this.constructor.create(),它会找到实例所属的类,子类实例就调到子类的版本。直接写 Animal.create() 也行,但会把类名写死,子类无法覆盖。
静态方法适合放什么?
和具体实例无关的逻辑,比如工厂方法 User.fromJSON()、工具函数 Array.isArray()、Promise.all(),以及类级别的缓存和配置。
super 在静态方法和实例方法里指向一样吗?
不一样。静态方法里的 super 指向父类本身,也就是 Animal;实例方法里的 super 指向 Animal.prototype。所以静态方法里只能用 super 调父类的静态方法,调不到实例方法。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。