手写 new 的本质与两种实现:从 Arguments 到构造流程的完整拆解
在 JavaScript 的世界里,new 是一个神奇而“神秘”的关键词。
当你敲下 new Person('张三', 18) 的那一刻,表面上只是“创建一个对象”,但底层却发生了一系列严谨而精细的操作:
创建对象、绑定原型、指定 this、执行构造函数……最后再把对象返回。
理解 new,相当于真正理解 JS 面向对象体系的核心:构造函数 + 原型链 + this 绑定。
为什么我们需要学习手写 new?这不仅是一道面试题,更是一把通往 JavaScript 核心机制的钥匙。要掌握它,我们得先从一个常被忽视却至关重要的角色说起——arguments。正是这个函数内部的“隐形参数集合”,为我们实现动态调用提供了可能。随后,我们将从最朴素的模拟雏形出发,逐步构建出第一种完整、稳健的手写 new 实现;再进一步,借助 arguments 与 shift 的巧妙组合,探索第二种更贴近底层逻辑的进阶写法。通过对比这两种方式,我们不仅能看清 new 的运行本质,还能深入理解 JavaScript 对象系统的构造规律。最终你会发现:当你真正理解了 new,也就真正理解了 JavaScript 是如何“造物”的。
一、为什么要学习 new?
学习 new 并不是为了“面试”,而是为了彻底理解 JavaScript 的对象系统。
new 涵盖 JS 的三大本质:
- 构造函数如何工作
- 原型链如何建立
- this 如何绑定
学习手写 new,会让你真正理解下面这段代码到底发生了什么:
let p = new Person('张三', 18);
如果把 JavaScript 比作厨房,new 就像是“做菜的整个流程”:
准备食材(创建对象)、把食材摆在厨台上(this 指向)、放调味料(构造函数执行)、最后端盘(返回对象)。
理解 new,你就理解了 JS 如何“做对象”。
二、理解 new 之前必须理解 arguments
1. arguments 是什么?
arguments 是 函数运行时的一个类数组对象,表示传进来的所有参数。
我们有这样一个例子:
function add() {
let result = 0;
for (let i = 0; i < arguments.length; i++) {
result += arguments[i];
}
return result;
}
console.log(add(1,2));
console.log(add(1,2,3));
运行后:
3
6
很明显,即使没有在函数定义中写参数,仍然可以通过 arguments 获取传入的所有值。
2. arguments 有哪些“类数组”特征?
arguments 像数组,但不是数组,它具有:
- 有 length
- 可以用索引访问,如
arguments[0] - 但不能用数组方法:
map、reduce、join…
例如: 当我们对arguments调用reduce方法时
console.log(arguments.reduce(((prev,cur)=> prev + cur),0));
得到的结果是:
它就像一个“冒牌数组”,看起来很像,但没有正式的“身份证”。
例如:
console.log(Object.prototype.toString.call(arguments));
console.log(Object.prototype.toString.call([1,2,3]));
输出:
[object Arguments]
[object Array]
这就是“arguments 是类数组,不是数组”的证据。
3. 为什么要介绍 arguments?
因为——
在模拟 new 的过程中,arguments 是关键角色。
比如:
function Person (name,age){
this.name = name;
this.age = age;
}
function objectFactory(Constructor,...args){
console.log(arguments[0]);
console.log(...args);
}
objectFactory(Person,'郑志鹏',18);
这里 Person 是构造函数,后面是其参数。
在模拟 new 的函数内部,我们需要:
- 获取构造函数(arguments[0])
- 获取构造函数的参数(arguments[1]…arguments[n])
所以 arguments 是“手写 new 必须掌握的第一块积木”。
三、用arguments初窥“模拟 new”的雏形
给出一个最粗糙的 new 模拟:
function objectFactory(){
var obj = new Object();
console.log(arguments,arguments[0]);
return obj;
}
如果我们调用:
let zzp = objectFactory(Person,'郑志鹏',18);
输出了:
也就是说:
- arguments[0] 是 Person 构造函数
- arguments[1]、arguments[2] 是构造参数
这告诉我们:
手写 new 时,我们可以用 arguments 来提取构造函数和构造函数参数。
但在此版本中,还缺少关键步骤:
- 这只是返回了一个普通对象,没有给它绑定原型
- 也没有执行构造函数
- 更没有把 this 和 obj 绑定起来
就好像一个“没灵魂的躯壳”,只是一个空对象而已。
下一步我们将正式进入真正的 new 操作流程。
四、第一种手写 new 的完整实现(核心版)
现在进入重头戏——
new 的核心实现。
给出一个极其标准且清晰的手写 new:
function objectFactory(Constructor,...args){
var obj = new Object(); // 1. 创建空对象
obj.__proto__ = Constructor.prototype; // 2. 原型绑定
Constructor.apply(obj,args) // 3. this 绑定并执行构造函数
return obj; // 4. 返回对象
}
function Person (name,age){
this.name = name;
this.age = age;
}
Person.prototype.species = '人类';
Person.prototype.sayHi = function(){
console.log(`你好,我是${this.name}`);
}
let zzp = objectFactory(Person,'郑志鹏',18);
console.log(zzp.age,zzp.species);
我们逐行理解。
步骤 1:创建空对象
var obj = new Object();
就像把一个“空盘子”端到你面前。
步骤 2:绑定原型(继承发生的地方)
obj.__proto__ = Constructor.prototype;
这是关键步骤!
new 做的事情之一就是让新对象可以访问构造函数的原型:
p.sayHi();
如果不绑定原型,这些方法就无法访问。
对象继承的能力就像是孩子继承了父亲的工具箱。
这行代码就是把工具箱放到了新对象的身上。
步骤 3:执行构造函数,让 this 指向新对象
Constructor.apply(obj,args);
这一步相当于:
obj.name = name;
obj.age = age;
构造函数内部所有挂在 this 上的属性,都会加到 obj 上。
步骤 4:返回新对象
return obj;
全部流程完成。
效果验证
console.log(zzp.age,zzp.species);
输出:
18
人类
这说明:
- 构造函数执行成功(age 存在)
- 原型挂载成功(species 存在)
到这里,你已经写出了一个 可用的 new 的实现。
五、第二种手写 new(进阶版:shift + arguments)
第二种实现更“灵活”,也更“JS 味”:
function objectFactory(){
var obj = new Object();
var Constructor = [].shift.call(arguments);
Constructor.apply(obj,arguments);
obj.__proto__ = Constructor.prototype;
return obj;
}
相比上一版,它做了三件更高级的事:
1. 使用 shift 提取构造函数
var Constructor = [].shift.call(arguments);
类似于:
var Constructor = arguments[0];
arguments = ['郑志鹏', 18];
也就是说:
- arguments[0] 是构造函数
- 剩下的是构造参数
将 arguments 修改后,后续 apply 变得更自然:
Constructor.apply(obj, arguments);
就像自动帮你“切掉第一个元素”,简化逻辑。
2. 先 apply 再绑定原型(顺序不同但效果一致)
在上一版中,顺序是:
先绑原型 → 再执行构造函数
在这里顺序是:
先执行构造函数 → 再绑原型
对于大多数构造函数来说,这 不影响最终效果。
顺序不同的原因是:
本版手写 new 更注重代码结构简洁。
3. 少了展开语法的依赖
上一版使用:
...args
而本版直接使用 arguments,不需要扩展语法。
这让它更接近“原始 JS 写法”。
效果验证
let zzp = objectFactory(Person,'郑志鹏',18);
console.log(zzp.age,zzp.species);
输出与上一版一致:
18
人类
功能仍然完整。
六、对比两种手写 new,理解底层规律
| 特点 | 第一种 | 第二种 |
|---|---|---|
| 参数处理 | 使用剩余参数(...args) | 使用 arguments + shift |
| 代码风格 | 清晰、现代 | 灵活、偏底层 |
| 原型绑定顺序 | apply 前 | apply 后 |
| 可读性 | 更易读 | 更“技巧性” |
核心都一样:
- 创建空对象
- 绑定原型
- 构造函数执行,绑定 this
- 返回对象
顺序略有差别,但大体一致。
七、结语:当你理解了 new,你也理解了 JavaScript 的对象系统
手写 new 并不是为了机械记忆代码,而是为了真正理解:
- 为什么对象能访问原型上的方法?
- 为什么构造函数里的 this 指向实例?
- 为什么 new 会返回一个对象?
当你意识到:
new 不是“语法糖”,而是一套流程
你的 JS 面向对象知识将会建立扎实的基础。
你可以自己实现 new,你就能理解 JavaScript 对象系统的灵魂。