手写 `new`:揭开 JavaScript 实例化背后的秘密

在 JavaScript 中,new 是一个看似简单却承载着面向对象核心机制的关键字。当我们写下 new Person() 时,引擎并非只是"调用一个函数",而是在背后完成了一系列精密的初始化操作:创建对象、绑定原型、执行构造逻辑、返回实例。理解这一过程,不仅能帮助我们掌握原型继承的本质,也为实现高级框架(如 Vue 的响应式系统)打下基础。本文将从零开始,手写一个 new 的模拟函数,并深入剖析其每一步的作用。

new 到底做了什么?

使用 new 调用构造函数时,JavaScript 引擎会按以下顺序执行:

  1. 创建一个全新的空对象;
  2. 将该对象的 __proto__ 指向构造函数的 prototype
  3. 将构造函数内部的 this 绑定到这个新对象,并执行函数体;
  4. 如果构造函数没有显式返回一个对象,则自动返回新创建的对象。

这四步构成了 JavaScript 原型式面向对象的基石。为了验证我们的理解,可以尝试手动复现这一过程。

手写 objectFactory 模拟 new

ini 复制代码
function objectFactory() {
  const obj = {};
  const Constructor = Array.prototype.shift.call(arguments);
  obj.__proto__ = Constructor.prototype;
  Constructor.apply(obj, arguments);
  return obj;
}

这段代码精炼地还原了 new 的行为。首先,通过 shiftarguments 中取出第一个参数(即构造函数),其余参数作为实参传递给构造函数。接着,将新对象的原型链指向构造函数的 prototype,确保实例能访问其方法。最后,用 applythis 绑定到 obj 并执行构造逻辑。

测试如下:

ini 复制代码
function Person(name, age) {
  this.name = name;
  this.age = age;
}
Person.prototype.sayHi = function() {
  console.log('你好,我是' + this.name);
};

const p1 = new Person('张三', 18);
const p2 = objectFactory(Person, '张三', 18);
console.log(p1.sayHi === p2.sayHi); // true

两个实例不仅属性一致,连原型方法也完全共享,证明我们的模拟是成功的。

关于 arguments:类数组的灵活与局限

objectFactory 中,我们使用了 arguments 对象来获取传入的所有参数。arguments 是函数内部的一个类数组对象 :它拥有 length 属性,可通过索引访问每个参数,但不具备数组的原生方法(如 mapreduce)。

javascript 复制代码
function add() {
  const args = [...arguments];
  return args.reduce((sum, val) => sum + val, 0);
}
console.log(add(1, 2, 3)); // 6

通过扩展运算符 [...arguments],我们可以将其转换为真正的数组,从而使用现代数组方法。这种灵活性使得 JavaScript 函数天然支持可变参数,无需预先声明参数个数。

为什么需要手动处理 arguments

在模拟 new 时,无法预知用户会传入多少个参数,也无法提前定义形参列表。因此,必须依赖 arguments 动态获取所有实参。而 Array.prototype.shift.call(arguments) 是一种经典技巧:它将 arguments 视为数组,从中"弹出"第一个元素(构造函数),剩余部分自然成为构造函数的参数列表。

值得注意的是,arguments 并非真正的数组,其 __proto__ 指向 Object.prototype,而非 Array.prototype。这也是为何不能直接调用 arguments.shift()------必须借助 callapply 借用数组的方法。

返回值的边界情况

标准 new 运算符还有一个细节:如果构造函数显式返回一个对象,则忽略新创建的实例,直接返回该对象。例如:

javascript 复制代码
function Test() {
  this.value = 1;
  return { value: 2 };
}
console.log(new Test().value); // 2

我们的 objectFactory 目前未处理此情况。若要完全兼容,可补充判断:

ini 复制代码
const result = Constructor.apply(obj, arguments);
return (typeof result === 'object' && result !== null) ? result : obj;

但在大多数实际场景中,构造函数不返回值或仅返回基本类型,因此简化版已足够使用。

总结

手写 new 不仅是一道常见的面试题,更是深入理解 JavaScript 对象模型的关键实践。通过模拟其实例化过程,我们清晰地看到:对象的创建、原型的链接、上下文的绑定 是如何协同工作的。同时,对 arguments 的操作也展示了 JavaScript 在参数处理上的动态特性。

尽管现代开发中 class 语法已普及,但其底层依然依赖 new 和原型链。掌握这些原始机制,意味着你不仅能写出更健壮的代码,还能在阅读框架源码、调试复杂继承关系时游刃有余。毕竟,真正的 JavaScript 功力,往往藏在这些"基础却深刻"的细节之中。

相关推荐
八角丶1 小时前
Node 网络编程 —— TLS 模块
javascript·后端·node.js
倔强的石头1061 小时前
【Linux指南】动静态库系列(七):符号表与重定位:链接器如何把函数调用接起来
linux·前端·javascript
图扑软件2 小时前
中篇・运笔|统一 DataModel 底座,HT UI 组件万物同源
javascript·低代码·ui·性能优化·数据可视化
小小尚@3 小时前
WebGIS/ECharts 地图开发|MapLand 在线行政区划边界一键下载 GeoJSON 工具
前端·javascript·echarts
葡萄城技术团队3 小时前
工业数据可视化:使用活字格 AIcoding + Three.js 构建轻量化三维设备监控大屏
开发语言·javascript·信息可视化
默_笙4 小时前
🚍 一条 Todo 的奇幻漂流:TypeScript 全栈类型安全的"护照检查"
前端·javascript
weixin_422555425 小时前
el-menu子菜单点击就被激活,恢复之前的激活状态
javascript·vue.js·elementui
Tanjia_kiki7 小时前
谷歌浏览器中F12编辑并重发请求
开发语言·前端·javascript
honkun67 小时前
vue 表格组件 vxe-table 实现拖拽列字段自动生成透视表汇总
前端·javascript·vue.js
IMPYLH8 小时前
HTML 的 <p> 元素
前端·javascript·html