手写 `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 功力,往往藏在这些"基础却深刻"的细节之中。

相关推荐
A24207349302 小时前
深入理解JS DOM:从基础操作到性能优化的全面指南
开发语言·javascript·ecmascript
布局呆星2 小时前
Vue 3 事件处理与列表渲染---02
前端·javascript·vue.js
syt_10132 小时前
设计模式之-状态模式
javascript·设计模式·状态模式
未寒2 小时前
关于uni app vue2 和vue3 的区别
前端·javascript·vue.js·uni-app
Aevget2 小时前
DevExtreme JS & ASP.NET Core v25.2新功能预览 - 字体栈、可访问性升级增强
javascript·asp.net·界面控件·devexpress·ui开发·devextreme
IT古董2 小时前
企业级官网全栈(React·Next.js·Tailwind·Axios·Headless UI·RHF·i18n)实战教程-第四篇:登录与注册系统(核心篇)
javascript·react.js·ui
q150803962252 小时前
数据整理无忧:深度评测高效文本合并工具的实用功能
开发语言·前端·javascript
华仔啊2 小时前
async/await 到底要不要加 try-catch?异步错误处理最佳实践
前端·javascript
开发者小天2 小时前
React中useCallback的使用
前端·javascript·react.js·typescript·前端框架·css3·html5