1. 为什么“构造函数里的方法”值得单独写一篇
很多人学JavaScript的时候,对构造函数的第一印象就是 function Person(name) { this.name = name; },然后 new Person('张三') 创建一个对象。但是等到真正写业务、看源码、或者是准备面试的时候,突然发现“构造函数”这个看似基础的概念,居然能延展出好几种方法的定义方式,而且每种方式的行为还不太一样。
我见过不少开发者在项目里把方法直接写在构造函数内部,过一阵子发现每个实例都维护了一份函数副本,内存开销上去了;也有人一律把所有方法都怼到 prototype 上,结果碰到需要访问实例私有变量的时候傻了眼;还有一部分人直接用 ES6 的 class 语法,却搞不清楚 class 里的方法到底是“自己的”还是“原型上的”。
这篇文章我就围绕“构造函数模式下的方法定义”这件事,把最常见的三种方式——构造函数内部使用 this 定义方法、原型 prototype 上定义方法、ES6 class 中定义方法——掰开揉碎讲清楚。它们是什么、怎么写、内存表现如何、什么时候用哪种、面试官一般怎么追问,全部放在一篇文章里。内容适合刚开始学 JavaScript 基础的朋友,也适合准备前端面试、或者想把自己代码写得更有章法的开发者参考。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 先搞懂一件事:构造函数里的 this 到底指向谁
要理解三种方法定义方式的区别,第一步不是看代码怎么写,而是要先把 new 关键字背后发生的事情想明白。很多人背了“new 会创建一个新对象”这句话,但不知道这个对象是怎么和 this 挂钩的。
当你写出 const p1 = new Person('张三') 的时候,JavaScript 引擎内部做了四件事:
- 创建一个全新的空对象;
- 把这个空对象的原型(
__proto__)指向Person.prototype; - 让
Person函数体内的this指向这个新对象; - 如果构造函数没有显式返回一个对象,就把这个新对象作为
new表达式的结果返回。
第 2 步和第 3 步是最关键的。第 2 步决定了“挂在原型上的方法,实例能通过原型链找到”;第 3 步决定了“写在构造函数里的 this.xxx,会直接挂到新对象自己身上”。
所以你会发现,“构造函数内部用 this 定义方法”和“在 prototype 上定义方法”,本质上是在两个不同的位置存放函数。前者放到了每个实例自己的内存里,后者放到了所有实例共享的原型对象上。这个差异直接带来三个深远影响:内存占用、方法是否能访问实例私有变量、子类继承时的行为。
我见过不少初学者把这两个位置搞混,写出来的代码逻辑没错,但从内存和设计角度怎么看都不对劲。所以后面每一个方法定义方式,我都会从“它到底把函数放哪了”这个底层视角开始讲。
3. 三种方法定义方式逐一拆解
3.1 第一种:构造函数内部通过 this 定义方法
最直观的一种写法,就是在构造函数体里面给 this 挂一个函数:
javascript复制function Person(name, age) {
this.name = name;
this.age = age;
this.sayName = function () {
console.log(`我是 ${this.name}`);
};
}
const p1 = new Person('张三', 20);
const p2 = new Person('李四', 22);
p1.sayName(); // 我是 张三
p2.sayName(); // 我是 李四
console.log(p1.sayName === p2.sayName); // false
最后一行比较结果 false,这就是这种写法的核心特征:每次 new 一个实例,都会重新创建一份 sayName 函数。哪怕这个函数逻辑完全一样,它在内存里也是两个独立的函数对象。
优点是什么呢?好处是每个实例都拥有一份完全独立的方法,如果方法内部需要访问构造函数的局部变量(不是 this 上的属性),就只能靠这种写法捕获。举个例子:
javascript复制function Person(name) {
// 局部变量,外部无法直接访问
let secret = '秘密';
this.name = name;
this.getSecret = function () {
return secret;
};
}
const p1 = new Person('张三');
console.log(p1.getSecret()); // 秘密
这种“闭包私有变量”的能力,是原型方法做不到的。因为原型方法在执行时,this 指向调用它的实例,但函数体的作用域链是从原型对象那里继承的,它拿不到构
