先看题目:
class A {
static a() {
console.log('a');
}
a() {
console.log('b');
}
}
A.prototype.a = function () {
console.log('c');
};
const obj = new A();
A.a();
obj.a();
这段代码的输出是:
a
c
为什么实例方法 a 明明在类里打印的是 b,最后却输出了 c?为什么 A.a() 又能打印 a?
要回答这两个问题,需要把 class 还原成它背后的构造函数和原型。
一、class 只是原型机制的语法糖
上面的类,大致等价于下面这段代码:
function A() {}
A.a = function () {
console.log('a');
};
A.prototype.a = function () {
console.log('b');
};
class 让写法更清晰,但没有改变底层模型:
static方法挂在构造函数A自己身上。- 普通实例方法挂在
A.prototype上。 new A()创建的对象,通过原型链继承A.prototype。
可以画成这样:
A(构造函数)
├── a() // static 方法,打印 a
└── prototype
└── a() // 实例方法,打印 b
而 obj 的结构是:
obj
└── [[Prototype]] ───> A.prototype
└── a()
obj 本身没有 a 属性。调用 obj.a() 时,JavaScript 会沿着原型链查找:
obj自己有没有a?没有。obj.__proto__,也就是A.prototype上有没有a?有。- 找到后执行。
二、A.a() 为什么打印 a
static a() 定义的是构造函数 A 的静态方法。
静态方法不会挂到 A.prototype 上,所以实例对象 obj 访问不到它。它属于 A 本身:
console.log(A.a); // [Function: a]
console.log(obj.a === A.a); // false
console.log(Object.hasOwn(A, 'a')); // true
调用 A.a() 时,A 在自己的属性里找到了静态方法 a,所以打印 a。
可以把它类比成“类级别的方法”:它不依赖具体实例,通常用来做工具函数、工厂方法或与类相关的全局操作。
三、obj.a() 为什么不是 b,而是 c
类里的普通方法 a() 会被放到 A.prototype 上:
class A {
a() {
console.log('b');
}
}
console.log(A.prototype.a); // [Function: a]
但紧接着执行了:
A.prototype.a = function () {
console.log('c');
};
这是一次普通的属性赋值。它把 A.prototype 上原本指向 b 的函数,替换成了打印 c 的新函数。
所以当后面执行 obj.a() 时,原型链上找到的 a 已经是新函数:
A.prototype.a ───> function () { console.log('c') }
因此输出 c,而不是 b。
类里定义的 b 并没有“消失”,只是它所在的属性被覆盖了。可以在覆盖前保存原方法:
const originalA = A.prototype.a;
A.prototype.a = function () {
console.log('c');
};
originalA(); // b
四、实例是动态查找,不是复制方法
有一个容易混淆的点:obj 是在覆盖之后才创建的,所以它拿到 c 很自然。但如果 obj 在覆盖之前创建,结果会变吗?
const obj = new A();
A.prototype.a = function () {
console.log('c');
};
obj.a(); // c
仍然是 c。
因为 new A() 不会把原型上的方法复制一份到实例上。实例只保存自己的属性,方法查找发生在调用时。只要原型上的属性被替换,之后所有实例的方法调用都会看到新值。
这也意味着,如果实例自己有一个同名属性,它会优先于原型:
const obj = new A();
obj.a = function () {
console.log('instance');
};
obj.a(); // instance
查找顺序是:
obj 自有属性
↓ 没找到
A.prototype
↓ 没找到
Object.prototype
↓ 没找到
null
五、静态方法和实例方法的区别
| 对比项 | static 方法 | 实例方法 |
|---|---|---|
| 挂载位置 | 构造函数本身 | Constructor.prototype |
| 调用方式 | A.a() | new A().a() |
| 实例能否直接访问 | 不能 | 能,通过原型链 |
| 常见用途 | 工具函数、工厂方法、类级别操作 | 操作实例状态、实例行为 |
| 是否依赖实例 | 通常不依赖 | 通常依赖 this |
注意,静态方法里的 this 指向构造函数,而实例方法里的 this 指向调用它的实例:
class A {
static whoAmI() {
console.log(this === A);
}
whoAmI() {
console.log(this instanceof A);
}
}
A.whoAmI(); // true
new A().whoAmI(); // true
两者可以同名,因为它们根本不在同一个对象上:
A.a; // static 方法
A.prototype.a; // 实例方法
六、如果想让 obj.a() 打印 b,应该怎么做
方法一:不要覆盖原型方法
删除这行:
A.prototype.a = function () {
console.log('c');
};
那么 obj.a() 就会沿着原型链找到类里定义的 b。
方法二:在覆盖前调用
const obj = new A();
obj.a(); // b
A.prototype.a = function () {
console.log('c');
};
obj.a(); // c
这能直观看到:覆盖的是原型上的方法,不是实例上的方法。
方法三:给实例添加同名方法
const obj = new A();
obj.a = function () {
console.log('instance');
};
obj.a(); // instance
实例自有属性优先级高于原型属性。
七、面试时怎么回答
可以按这个顺序说:
- 输出是
a和c。 static a()挂在构造函数A上,所以A.a()打印a。- 普通方法
a()挂在A.prototype上,实例通过原型链访问。 A.prototype.a = function () { ... }覆盖了原型上的原方法。obj.a()在原型链上找到的是覆盖后的函数,所以打印c。- 实例不会复制原型方法,方法是在调用时动态查找的。
如果再补充一句“class 本质上还是构造函数加原型链”,这道题的原理就讲清楚了。
最后
这道题考察的不是 class 的语法,而是两个基础模型:
static方法属于构造函数,实例访问不到。- 普通方法属于原型,实例通过原型链查找。
记住一句话:A.a 和 A.prototype.a 是两个不同的属性。 一个给类用,一个给实例用。理解它们的位置,就不会被同名方法和后续覆盖搞混。