当前位置:首页 > 技术 > 正文内容

JavaScript new 关键字:深入底层机制

访客 技术 1

大多数开发者每周都会使用 new 数十次,却很少思考其背后的原理。你写下 new Promise()new Map()new Person()——它们总能正常工作。直到某一天它突然失效,你盯着 undefined,不明白哪里出了问题。

以下情况是否似曾相识?

  • 你在构造函数前忘了写 new,返回了 undefined 而不是对象——完全摸不着头脑
  • 你在控制台见过 __proto__prototype,然后默默关掉了标签页
  • 你知道 new 会"创建对象",但无法解释从调用到返回之间究竟发生了什么
  • 你用过 JavaScript 类,却对其背后的机制感到不安

问题不在于你能力不足。 new 关键字触发的是一个多步骤过程,而 JavaScript 完全隐藏了它——大多数教程也直接跳过。

如果你曾觉得 new 是个黑盒,那么这篇文章正是为你准备的。

✅ 你将学到什么

  • new 关键字实际做了什么——四个步骤清晰讲解
  • 为什么原型链连接很重要,以及它如何关联你创建的每个对象
  • 构造函数如何工作,以及它们与现代 ES6 类的关系
  • 忘记 new 时会如何静默破坏代码(以及如何预防)
  • 这种模式在 React 等真实库和原生 API(如 PromiseMap)中的应用
  • 如何构建自己的心智模型,让 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.prototypetrue,就像你自己的构造函数一样。

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 已经清晰,以下方向可以帮助你加深理解:

  1. 原型链与继承 —— 理解 Object.create() 如何工作,以及如何在不使用类的情况下构建多层继承。这是 JavaScript 对象模型的基础。
  2. JavaScript 中的 this —— new 只是 this 绑定四种方式之一。学习显式绑定(.call().apply().bind())、隐式绑定和箭头函数,彻底掌握 this
  3. 深入 ES6 类 —— 既然你理解了类编译后的实质,可以充满信心地探索 extendssuper()、静态方法和私有字段。
  4. 对象组合 vs 继承 —— 熟悉原型后,探索为什么许多经验丰富的开发者更喜欢组合而非经典继承——以及 mixin 模式如何提供强大的替代方案。

有疑问?

欢迎在评论区留言!我很想知道这篇文章在哪里让你豁然开朗,或者你还有哪些不确定的地方——两者都值得讨论。

未来文章的主题:

  • 原型链可视化:深入 __proto__Object.prototype 以及为什么 null 位于每条链的顶端
  • 掌握 JavaScript 中的 this:所有四种绑定规则,包含真实 bug 及其修复
  • Object.create() vs new vs 类语法:何时使用哪一种及其权衡
  • 清晰理解 JavaScript 继承:如何正确使用 extendssuper,以及何时组合是更好的选择

祝你编码愉快!🚀

返回列表

上一篇:深入理解 JavaScript 回调函数及其必要性

没有最新的文章了...

相关文章

Linux crontab 详解

1) crontab 是什么cron 是 Linux 的定时任务守护进程;crontab 是用来编辑/查看“按时间周期执行命令”的表(cron table)。常见两类:用户 crontab:每个用户一份(crontab -e 编辑)系统级 crontab / cron.d:可指定执行用户(/etc/crontab、/etc/cron.d/*)2) crontab 时间...

富文本里可以允许的 HTML 属性

一、所有标签默认允许的安全属性(极少)class        (可选)id           (通常建议禁用)title️ 注意:id 容易被滥用做锚点注入,很多系统直接禁用class 允许的话最好只允许固定前缀(如 editor-*)二、a 标签允许属性<a href="" t...

Mac 安装 Node.js 指南

方法一:通过官网安装包(最简单,适合初学者)如果你只是想快速安装并开始使用,这是最直接的方法。访问 Node.js 官网。页面会显示两个版本:LTS (Recommended For Most Users):长期支持版,最稳定。建议选这个。Current:最新特性版,包含最新功能但可能不够稳定。下载 .pkg 安装包并运行。按照安装向导点击“下一步”即可完成。方法二:使用 Homebrew 安装(...

Dom\HTML_NO_DEFAULT_NS 的副作用:自动加闭合标签

在使用Dom\HTMLDocument时,Dom\HTML_NO_DEFAULT_NS 将禁止在解析过程中设置元素的命名空间, 此设置是为了与DOMDocument向后兼容而存在的。当使用它时,已知的一个副作用就是:自动加闭合标签例如 </img> 为什么会这样?当你使用:Dom\HTML_NO_DEFAULT_NS文档会变成 无命名空间模式,此时内部更接近 XML...

Laravel 事件和监听器创建

在 Laravel 中,使用 Artisan 命令创建 Events(事件) 和 Listeners(监听器) 是非常高效的。你可以通过以下几种方式来实现:1. 手动创建单个 Event如果你只想创建一个事件类,可以使用 make:event 命令:Bashphp artisan make:event UserRegistered执行后,文件将生成在 app/Even...

自定义域名解析神器 dnsmasq

什么是 dnsmasq?dnsmasq 是一个轻量级、功能强大的网络服务工具,专为小型和中等规模网络设计。它是一个综合的网络基础设施解决方案[1]。dnsmasq 能做什么?功能说明应用场景DNS 转发与缓存将 DNS 查询转发到上游服务器(ISP、Google DNS 等),并在本地缓存结果加快 DNS 查询速度,减少外部 DNS 流量本地 DNS解析本地网络设备的主机名,无需编辑&n...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。