彻底搞懂 JavaScript 的 new 到底在干什么?手撕 new + Arguments 核心原理解析
在面试中,「手写 new 的实现」和「arguments 到底是个啥」几乎是中高级前端的必考题。
今天我们不背答案,而是把它们彻底拆开,看看 JavaScript 引擎在底层到底做了什么。

一、new 运算符到底干了哪四件事?
当你写下这行代码时:
const p =new Person('柯基', 18);
JavaScript 引擎默默为你做了 4 件大事:
- 创建一个全新的空对象
{}
- 把这个空对象的
__proto__ 指向构造函数的 prototype
- 让构造函数的
this 指向这个新对象,并执行构造函数(传入参数)
- 自动返回这个对象(除非构造函数显式返回了一个对象)
这就是传说中的“new 的四步走”。
很多人背得滚瓜烂熟,但真正问他为什么 __proto__ 要指向 prototype?为什么不能直接 obj.prototype = Constructor.prototype?就懵了。
关键提醒(易错点!)
// 错误写法!千万别这样写!
obj.prototype = Constructor.prototype;
// 正确写法
obj.__proto__ = Constructor.prototype;
因为 prototype 是构造函数才有的属性,实例对象根本没有 prototype!
所有对象都有 __proto__(非标准,已被 [[Prototype]] 内部槽替代,现代浏览器用 Object.getPrototypeOf),它是用来查找原型链的。
手撕一个完美版 new
function myNew(Constructor, ...args) {
// 1. 创建一个空对象
const obj = Object.create(Constructor.prototype);
// 2 & 3. 执行构造函数,绑定 this,并传入参数
const result = Constructor.apply(obj, args);
// 4. 如果构造函数返回的是对象,则返回它,否则返回我们创建的 obj
return result instanceof Object ? result : obj;
}
为什么这里用 Object.create(Constructor.prototype) 而不是 new Object() + 设置 __proto__?
因为 Object.create(proto) 是最纯粹、最推荐的建立原型链的方式,比手动操作 __proto__ 更现代、更安全。
验证一下
function Dog(name, age) {
this.name = name;
this.age = age;
}
Dog.prototype.bark = function() {
console.log(`${this.name} 汪汪汪!`);
};
const dog1 = new Dog('小黑', 2);
const dog2 = myNew(Dog, '大黄', 3);
dog1.bark(); // 小黑 汪汪汪!
dog2.bark(); // 大黄 汪汪汪!
console.log(dog2 instanceof Dog); // true
console.log(Object.getPrototypeOf(dog2) === Dog.prototype); // true
完美复刻!
二、arguments 是个什么鬼?
你可能写过无数次函数,却不知道 arguments 到底是个啥玩意儿。
function add(a, b, c) {
console.log(arguments);
// Arguments(5) [1, 2, 3, 4, 5, callee: ƒ, Symbol(Symbol.iterator): ƒ]
}
add(1,2,3,4,5);
打印出来长得像数组,但其实不是!
类数组(Array-like)的三大特征
- 有
length 属性
- 可以用数字索引访问
arguments[0]、arguments[1]...
- 不是真正的数组,没有
map、reduce、forEach 等方法
经典面试题:怎么把 arguments 变成真数组?
5 种方式,从老到新:
function test() {
// 方式1:Array.prototype.slice.call(arguments)
const arr1 = Array.prototype.slice.call(arguments);
// 方式2:[...arguments] 展开运算符(最优雅)
const arr2 = [...arguments];
// 方式3:Array.from(arguments)
const arr3 = Array.from(arguments);
// 方式4:用 for 循环 push(性能最好,但写法古老)
const arr4 = [];
for(let i = 0; i < arguments.length; i++) {
arr4.push(arguments[i]);
}
// 方式5:Function.prototype.apply 魔术(了解即可)
const arr5 = Array.prototype.concat.apply([], arguments);
}
推荐顺序:[...arguments] > Array.from() > 手写 for 循环
arguments 和箭头函数的恩怨情仇(超级易错!)
const fn = () => {
console.log(arguments); // ReferenceError!
};
fn(1,2,3);
箭头函数没有自己的 arguments!它会往上层作用域找。
这是因为箭头函数没有 [[Call]] 内部方法,所以也没有 arguments 对象。
arguments.callee 已经死了
以前可以这样写递归:
// 老黄历(严格模式下报错,已废弃)
function factorial(n) {
if (n <= 1) return 1;
return n * arguments.callee(n - 1);
}
现在请用命名函数表达式:
const factorial = function self(n) {
if (n <= 1) return 1;
return n * self(n - 1);
};
三、把所有知识点串起来:实现一个支持任意参数的 sum 函数
function sum() {
// 方案1:用 reduce(推荐)
return [...arguments].reduce((pre, cur) => pre + cur, 0);
// 方案2:经典 for 循环(性能最好)
// let total = 0;
// for(let i = 0; i < arguments.length; i++) {
// total += arguments[i];
// }
// return total;
}
console.log(sum(1,2,3,4,5)); // 15
console.log(sum(10, 20)); // 30
console.log(sum()); // 0
四、总结:new 和 arguments 的灵魂考点
| 考点 |
正确答案 & 易错点提醒 |
| new 做了哪几件事? |
4 步:创建对象 → 链接原型 → 绑定 this → 返回对象 |
| obj.proto 指向谁? |
Constructor.prototype(不是 Constructor 本身!) |
| 手写 new 推荐方式 |
Object.create(Constructor.prototype) + apply |
| arguments 是数组吗? |
不是!是类数组对象 |
| 如何转真数组? |
[...arguments] 最优雅 |
| 箭头函数有 arguments 吗? |
没有!会抛错 |
| arguments.callee |
已废弃,严格模式下报错 |

几个细节知识点
1.arguments 到底是什么类型的数据?
通过Object.prototype.toString.call 打印出 [object Arguments]
arguments 是一个 真正的普通对象(plain object),而不是数组! 它的内部类([[Class]])是 "Arguments",这是一个 ECMAScript 规范里专门为函数参数创建的特殊内置对象。
为什么它长得像数组?
因为 JS 引擎在创建 arguments 对象时,特意给它加了这些“伪装属性”:
JavaScript
arguments.length = 参数个数
arguments[0], arguments[1]... = 对应的实参
arguments[Symbol.iterator] = Array.prototype[Symbol.iterator] // 所以可以 for...of
这就是传说中的“类数组(array-like object)”。
2.apply 不仅可以接受数组,还可以接受类数组,底层逻辑是什么?
apply 的第二个参数只要求是一个 “Array-like 对象” 或 “类数组对象”,甚至可以是任何有 length 和数字索引的对象!
JavaScript
// 官方接受的类型统称为:arguments object 或 array-like object
func.apply(thisArg, argArray)
能传什么?疯狂测试!
JavaScript
function sum() {
return [...arguments].reduce((a,b)=>a+b);
}
// 这些全都可以被 apply 正确处理!
sum.apply(null, [1,2,3,4,5]); // 真数组
sum.apply(null, arguments); // arguments 对象
sum.apply(null, {0:1, 1:2, 2:3, length: 3}); // 自定义类数组对象
sum.apply(null, "abc"); // 字符串!也是类数组
sum.apply(null, new Set([1,2,3])); // 不行!Set 没有 length 和索引
sum.apply(null, {length: 5}); // 得到 [undefined×5]
所以只要满足:
- 有 length 属性(可转为非负整数)
- 有 0, 1, 2... 这些数字属性
就能被 apply 正确展开!
3.[].shift.call(arguments) 到底是什么鬼?为什么能取到构造函数?
这行代码堪称“手写 new 的经典黑魔法”:
JavaScript
function myNew() {
var Constructor = [].shift.call(arguments);
// 现在 Constructor 就是 Person,arguments 变成了剩余参数
}
myNew(Person, '张三', 18);
一步步拆解:
JavaScript
[].shift // Array.prototype.shift 方法
.call(arguments) // 把 arguments 当作 this 调用 shift
shift 的作用:删除并返回数组的第一个元素
因为 arguments 是类数组,所以 Array.prototype.shift 能作用于它!
执行过程:
JavaScript
// 初始
arguments = [Person函数, '张三', 18]
// [].shift.call(arguments) 执行后:
返回 Person 函数
arguments 变成 ['张三', 18] // 原地被修改了!
归根结底:这利用了类数组能借用数组方法的特性
所以这行代码一箭三雕:
- 取出构造函数
- 把 arguments 变成真正的剩余参数数组
- 不需要写 arguments[0], arguments.slice(1) 这种丑代码
最后送你一份面试加分答案模板
面试官:请手写实现 new 运算符
function myNew(Constructor, ...args) {
// 1. 用原型创建空对象(最推荐)
const obj = Object.create(Constructor.prototype);
// 2. 执行构造函数,绑定 this
const result = Constructor.apply(obj, args);
// 3. 返回值处理(常被忽略!)
return typeof result === 'object' && result !== null ? result : obj;
}
面试官:那 arguments 呢?
// 快速转换为真数组
const realArray = [...arguments];
// 或者
const realArray = Array.from(arguments);
一句「Object.create 是建立原型链最纯粹的方式」就能让面试官眼前一亮。
搞懂了 new 和 arguments,你就已经站在了 JavaScript 底层机制的肩膀上。