创见博客
JavaScript 类与原型:static 方法和实例方法到底挂在哪里?
七崽爱吃小饼干2026/10/19阅读 0

先看题目:

js
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();

这段代码的输出是:

text
a
c

为什么实例方法 a 明明在类里打印的是 b,最后却输出了 c?为什么 A.a() 又能打印 a?

要回答这两个问题,需要把 class 还原成它背后的构造函数和原型。

一、class 只是原型机制的语法糖

上面的类,大致等价于下面这段代码:

js
function A() {}

A.a = function () {
  console.log('a');
};

A.prototype.a = function () {
  console.log('b');
};

class 让写法更清晰,但没有改变底层模型:

  • static 方法挂在构造函数 A 自己身上。
  • 普通实例方法挂在 A.prototype 上。
  • new A() 创建的对象,通过原型链继承 A.prototype。

可以画成这样:

text
A(构造函数)
├── a()                 // static 方法,打印 a
└── prototype
    └── a()             // 实例方法,打印 b

而 obj 的结构是:

text
obj
└── [[Prototype]] ───> A.prototype
                        └── a()

obj 本身没有 a 属性。调用 obj.a() 时,JavaScript 会沿着原型链查找:

  1. obj 自己有没有 a?没有。
  2. obj.__proto__,也就是 A.prototype 上有没有 a?有。
  3. 找到后执行。

二、A.a() 为什么打印 a

static a() 定义的是构造函数 A 的静态方法。

静态方法不会挂到 A.prototype 上,所以实例对象 obj 访问不到它。它属于 A 本身:

js
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 上:

js
class A {
  a() {
    console.log('b');
  }
}

console.log(A.prototype.a); // [Function: a]

但紧接着执行了:

js
A.prototype.a = function () {
  console.log('c');
};

这是一次普通的属性赋值。它把 A.prototype 上原本指向 b 的函数,替换成了打印 c 的新函数。

所以当后面执行 obj.a() 时,原型链上找到的 a 已经是新函数:

text
A.prototype.a ───> function () { console.log('c') }

因此输出 c,而不是 b。

类里定义的 b 并没有“消失”,只是它所在的属性被覆盖了。可以在覆盖前保存原方法:

js
const originalA = A.prototype.a;

A.prototype.a = function () {
  console.log('c');
};

originalA(); // b

四、实例是动态查找,不是复制方法

有一个容易混淆的点:obj 是在覆盖之后才创建的,所以它拿到 c 很自然。但如果 obj 在覆盖之前创建,结果会变吗?

js
const obj = new A();
A.prototype.a = function () {
  console.log('c');
};
obj.a(); // c

仍然是 c。

因为 new A() 不会把原型上的方法复制一份到实例上。实例只保存自己的属性,方法查找发生在调用时。只要原型上的属性被替换,之后所有实例的方法调用都会看到新值。

这也意味着,如果实例自己有一个同名属性,它会优先于原型:

js
const obj = new A();
obj.a = function () {
  console.log('instance');
};

obj.a(); // instance

查找顺序是:

text
obj 自有属性
  ↓ 没找到
A.prototype
  ↓ 没找到
Object.prototype
  ↓ 没找到
null

五、静态方法和实例方法的区别

对比项static 方法实例方法
挂载位置构造函数本身Constructor.prototype
调用方式A.a()new A().a()
实例能否直接访问不能能,通过原型链
常见用途工具函数、工厂方法、类级别操作操作实例状态、实例行为
是否依赖实例通常不依赖通常依赖 this

注意,静态方法里的 this 指向构造函数,而实例方法里的 this 指向调用它的实例:

js
class A {
  static whoAmI() {
    console.log(this === A);
  }

  whoAmI() {
    console.log(this instanceof A);
  }
}

A.whoAmI();      // true
new A().whoAmI(); // true

两者可以同名,因为它们根本不在同一个对象上:

js
A.a;             // static 方法
A.prototype.a;   // 实例方法

六、如果想让 obj.a() 打印 b,应该怎么做

方法一:不要覆盖原型方法

删除这行:

js
A.prototype.a = function () {
  console.log('c');
};

那么 obj.a() 就会沿着原型链找到类里定义的 b。

方法二:在覆盖前调用

js
const obj = new A();
obj.a(); // b

A.prototype.a = function () {
  console.log('c');
};

obj.a(); // c

这能直观看到:覆盖的是原型上的方法,不是实例上的方法。

方法三:给实例添加同名方法

js
const obj = new A();

obj.a = function () {
  console.log('instance');
};

obj.a(); // instance

实例自有属性优先级高于原型属性。

七、面试时怎么回答

可以按这个顺序说:

  1. 输出是 a 和 c。
  2. static a() 挂在构造函数 A 上,所以 A.a() 打印 a。
  3. 普通方法 a() 挂在 A.prototype 上,实例通过原型链访问。
  4. A.prototype.a = function () { ... } 覆盖了原型上的原方法。
  5. obj.a() 在原型链上找到的是覆盖后的函数,所以打印 c。
  6. 实例不会复制原型方法,方法是在调用时动态查找的。

如果再补充一句“class 本质上还是构造函数加原型链”,这道题的原理就讲清楚了。

最后

这道题考察的不是 class 的语法,而是两个基础模型:

  • static 方法属于构造函数,实例访问不到。
  • 普通方法属于原型,实例通过原型链查找。

记住一句话:A.a 和 A.prototype.a 是两个不同的属性。 一个给类用,一个给实例用。理解它们的位置,就不会被同名方法和后续覆盖搞混。

评论
0/100