JavaScript new 关键字:深入底层机制
大多数开发者每周都会使用 new 数十次,却很少思考其背后的原理。你写下 new Promise()、new Map()、new Person()——它们总能正常工作。直到某一天它突然失效,你盯着 undefined,不明白哪里出了问题。
以下情况是否似曾相识?
- 你在构造函数前忘了写
new,返回了undefined而不是对象——完全摸不着头脑 - 你在控制台见过
__proto__和prototype,然后默默关掉了标签页 - 你知道
new会"创建对象",但无法解释从调用到返回之间究竟发生了什么 - 你用过 JavaScript 类,却对其背后的机制感到不安
问题不在于你能力不足。 new 关键字触发的是一个多步骤过程,而 JavaScript 完全隐藏了它——大多数教程也直接跳过。
如果你曾觉得 new 是个黑盒,那么这篇文章正是为你准备的。
✅ 你将学到什么
new关键字实际做了什么——四个步骤清晰讲解- 为什么原型链连接很重要,以及它如何关联你创建的每个对象
- 构造函数如何工作,以及它们与现代 ES6 类的关系
- 忘记
new时会如何静默破坏代码(以及如何预防) - 这种模式在 React 等真实库和原生 API(如
Promise、Map)中的应用 - 如何构建自己的心智模型,让 JavaScript 的对象系统豁然开朗
无需高级预备知识——只要了解基本的 JavaScript 函数和对象即可。
什么是构造函数?
在理解 new 之前,你需要知道它的工作对象。
构造函数就是一个普通的 JavaScript 函数,遵循一个约定:它用于创建并返回对象,且函数名首字母大写。
function Person(name, age) {
this.name = name;
this.age = age;
}
就是这样。这个函数在结构上并无特殊之处——首字母大写 P 纯粹是向其他开发者(以及你自己)发出的信号,表明它应该用 new 来调用。
不用 new 调用时,它就和普通函数一样。用 new 调用时,JavaScript 会激活一条完全不同的执行路径。这正是我们要理解的。
new 背后的四步过程
当你写下:
const user1 = new Person("Aman", 25);
你可能会认为 JavaScript 只是"填充"了对象。实际上,引擎会执行四个不同的步骤。让我们逐一分析。
第一步:创建一个全新的空对象
const obj = {};
JavaScript 在后台创建了一个全新的空对象。这个对象还没有任何属性——它是一张白纸,最终会成为 user1。
想象一下新员工入职第一天,桌上空无一物。构造函数即将把它布置好。
第二步:对象与原型链接
obj.__proto__ = Person.prototype;
这一步是大多数开发者忽略的——却也是最重要的。
每个 JavaScript 函数都自动拥有一个 prototype 属性。当你使用 new 时,新建的对象会秘密地链接到该原型。这意味着 user1 可以访问你在 Person.prototype 上定义的任何方法,即使这些方法并不直接存储在 user1 上。
想象一下公司手册:每位员工(实例)都有自己的办公桌(属性),但他们都共享同一本放在休息室里的手册(原型)。手册不会被复制给每个人——它只放在一处,所有人都能引用。
user1 → Person.prototype → Object.prototype → null
这条链称为原型链,是 JavaScript 处理继承的方式。当你访问属性或方法时,JS 会沿着这条链向上查找,直到找到(或到达 null)。
第三步:this 绑定到新对象
Person.call(obj, "Aman", 25);
现在构造函数开始执行——但 this 指向第一步创建的空对象。因此当构造函数执行:
this.name = name; // obj.name = "Aman"
this.age = age; // obj.age = 25
这些属性直接落在了 obj 上。此步骤之后,对象看起来像:
obj = { name: "Aman", age: 25 }
员工的办公桌就布置好了。
第四步:返回对象
return obj;
如果构造函数没有显式返回其他对象,JavaScript 会自动返回这个新建的对象。因此:
const user1 = new Person("Aman", 25);
// user1 现在是 { name: "Aman", age: 25 }
四步过程可视化:
new Person("Aman", 25)
│
▼
① 创建空对象:{}
│
▼
② 链接原型:obj.__proto__ = Person.prototype
│
▼
③ 绑定 this + 运行构造函数:obj.name = "Aman", obj.age = 25
│
▼
④ 返回对象 → user1
🏋️ 练习1:自行追踪步骤
考虑以下构造函数:
function Car(make, year) {
this.make = make;
this.year = year;
}
const myCar = new Car("Toyota", 2022);
在纸上(或注释中)写下每一步发生了什么。然后在浏览器控制台中运行 console.log(myCar) 和 console.log(myCar.__proto__ === Car.prototype) 验证。
预期输出:
{ make: 'Toyota', year: 2022 }
true
如果第二行输出 true,你就亲眼证实了原型链接。🎉
为什么原型链接如此强大?
既然知道对象会链接到原型,下面看看这在实际中的重要性。
假设你在构造函数内部直接定义了 greet 方法:
function Person(name) {
this.name = name;
this.greet = function() { // ⚠️ 不要这样做
console.log(`Hi, I'm ${this.name}`);
};
}
每次调用 new Person() 时,JavaScript 都会创建一个全新的 greet 函数副本并附加到该实例。创建 10,000 个用户,就会有 10,000 个一模一样的函数占用内存。
解决方法: 将共享方法定义在原型上。
function Person(name) {
this.name = name;
}
Person.prototype.greet = function() {
console.log(`Hi, I'm ${this.name}`);
};
现在所有实例通过原型链共享同一个 greet 函数。实例行为完全相同,但内存占用大幅减少。
const user1 = new Person("Aman");
const user2 = new Person("Riya");
user1.greet === user2.greet // ✅ true —— 同一个函数,通过原型共享
user1 === user2 // ✅ false —— 不同对象
关键洞察: 实例是唯一的(不同对象携带不同数据),行为是共享的(方法位于原型上,而非每个实例上)。
🏋️ 练习2:发现内存问题
function Button(label) {
this.label = label;
this.click = function() {
console.log(`${this.label} clicked`);
};
}
const b1 = new Button("Submit");
const b2 = new Button("Cancel");
console.log(b1.click === b2.click); // 输出什么?
运行这段代码,记录输出,然后重构,将 click 移到 Button.prototype 上。重构后,=== 检查应返回 true。
如果忘记 new 会发生什么?
这就是问题静默发生的地方。
const user1 = Person("Aman", 25); // 缺少 new!
没有 new,四个步骤都不会执行。没有新对象,没有原型链接,this 不会指向你期望的地方——在非严格模式下它指向全局对象(浏览器中是 window,Node.js 中是 global),在严格模式下会抛出 TypeError。
结果:
console.log(user1); // undefined
console.log(window.name); // "Aman" —— 你污染了全局作用域 😬
两种防范方法:
方法1——使用严格模式:
"use strict";
function Person(name) {
this.name = name; // 如果没有 new 调用则抛出 TypeError
}
方法2——自修正构造函数:
function Person(name) {
if (!(this instanceof Person)) {
return new Person(name); // 自动修复缺失的 new
}
this.name = name;
}
方法2更宽容——无论是否使用 new 都能工作。方法1会直接报错,这在大多数代码库中其实更好,因为它能尽早捕获错误。
一个陷阱:返回对象会覆盖一切
这是一个微妙的边界情况,即使经验丰富的开发者也会遇到。
如果构造函数显式返回一个对象,那么它会覆盖自动返回的实例:
function Person(name) {
this.name = name;
return { name: "Override" }; // ← 返回一个普通对象
}
const user = new Person("Aman");
console.log(user.name); // "Override" —— 不是 "Aman"!
但是,返回一个原始值(字符串、数字、布尔值)会被静默忽略——JavaScript 仍然返回构造的实例:
function Person(name) {
this.name = name;
return 42; // ← 返回原始值被忽略
}
const user = new Person("Aman");
console.log(user.name); // "Aman" —— 正常工作
规则: 只有从构造函数返回一个非 null 的对象才会覆盖默认行为。
这与现代 JavaScript 的关联
理解 new 不仅仅是历史知识——它能直接解释你每天使用的特性背后的原理。
ES6 类不过是伪装下的构造函数
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
greet() {
console.log(`Hi, I'm ${this.name}`);
}
}
这看起来像 Java 或 Python 中的经典面向对象编程,但 JavaScript 类只是基于原型的相同系统的语法糖。实际上,greet 被放在 Person.prototype 上,new Person() 仍然执行同样的四步过程。
你可以自行验证:
console.log(typeof Person); // "function" —— 它仍然是一个函数!
每天使用的原生 API
同样的机制驱动了许多内置 JavaScript API:
const myMap = new Map(); // 创建一个 Map 实例
const mySet = new Set([1, 2, 3]); // 创建一个 Set 实例
const p = new Promise((res) => {}); // 创建一个 Promise 实例
每个都经历了四步 new 过程。myMap.__proto__ === Map.prototype 为 true,就像你自己的构造函数一样。
React 类组件(Pre-Hooks 时代)
class MyComponent extends React.Component {
render() {
return <div>Hello</div>;
}
}
当 React 渲染一个类组件时,它在内部调用 new MyComponent(props)。React 的渲染引擎依赖基于原型的继承来区分类组件和函数组件——这正是在类组件中需要 extends React.Component 的原因。
🏋️ 练习3:实现自己的 new
真正理解 new 的最好方式是自己实现它。这是一个框架:
function myNew(Constructor, ...args) {
// 第一步:创建一个空对象
const obj = ???;
// 第二步:链接其原型
???.__proto__ = ???;
// 第三步:以 obj 为 this 运行构造函数
const result = ???;
// 第四步:返回 obj(除非构造函数返回了它自己的对象)
return ???;
}
// 测试:
function Person(name) {
this.name = name;
}
Person.prototype.greet = function() {
console.log(`Hi, I'm ${this.name}`);
};
const user = myNew(Person, "Aman");
console.log(user.name); // "Aman"
user.greet(); // "Hi, I'm Aman"
第二步提示: 使用 Object.create(Constructor.prototype) 而不是手动设置 __proto__——这是现代、符合规范的实现原型链接的方式。
如果你的 myNew 通过了两个断言,你就完全理解了 new 的工作原理。完成这个练习胜过阅读十篇相关文章。
你的心智模型:像引擎一样思考
从现在起,当你在代码中看到 new 时,在脑海里将它翻译成:
const x = new Foo(arg1, arg2);
① {} — 创建空对象
② obj.__proto__ = Foo.prototype — 链接到原型链
③ Foo.call(obj, arg1, arg2) — 运行构造函数,绑定 this
④ return obj — 返回完成的实例
内化这四个步骤,JavaScript 的一层复杂性就会消失:
| 概念 | 与 new 的联系 |
|---|---|
| ES6 类 | 构造函数 + 原型的语法糖 |
this 混淆 |
this 是第一步创建的新对象 |
| 原型链 | 第二步建立——始终存在 |
instanceof 操作符 |
检查 Constructor.prototype 是否在原型链中 |
Object.create() |
手动执行第二步(不运行构造函数) |
下一步学习什么
现在 new 已经清晰,以下方向可以帮助你加深理解:
- 原型链与继承 —— 理解
Object.create()如何工作,以及如何在不使用类的情况下构建多层继承。这是 JavaScript 对象模型的基础。 - JavaScript 中的
this——new只是this绑定四种方式之一。学习显式绑定(.call()、.apply()、.bind())、隐式绑定和箭头函数,彻底掌握this。 - 深入 ES6 类 —— 既然你理解了类编译后的实质,可以充满信心地探索
extends、super()、静态方法和私有字段。 - 对象组合 vs 继承 —— 熟悉原型后,探索为什么许多经验丰富的开发者更喜欢组合而非经典继承——以及 mixin 模式如何提供强大的替代方案。
有疑问?
欢迎在评论区留言!我很想知道这篇文章在哪里让你豁然开朗,或者你还有哪些不确定的地方——两者都值得讨论。
未来文章的主题:
- 原型链可视化:深入
__proto__、Object.prototype以及为什么null位于每条链的顶端 - 掌握 JavaScript 中的
this:所有四种绑定规则,包含真实 bug 及其修复 Object.create()vsnewvs 类语法:何时使用哪一种及其权衡- 清晰理解 JavaScript 继承:如何正确使用
extends和super,以及何时组合是更好的选择
祝你编码愉快!🚀