深入理解 JavaScript:从 arguments 到手写 new
在 JavaScript 的世界里,函数不仅是“可调用的对象”,更是动态、灵活的构造单元。本文将带你从一个看似简单的 add 函数出发,逐步揭开 arguments 对象的本质、类数组与真数组的区别,并最终实现一个能完全模拟 new 操作符的 objectFactory 函数——这正是面试高频题“手写 new”的完整解法。
一、起点:一个可变参数的加法函数
我们先看这段常见代码:
function add() {
let result = 0;
for (let i = 0; i < arguments.length; i++) {
result += arguments[i];
}
return result;
}
console.log(add(1, 2)); // 3
console.log(add(1, 2, 3)); // 6
这个函数没有声明任何形参,却能接收任意数量的实参并求和。秘密就在于 arguments。
✅ arguments 是什么?
- 它是普通函数内部自动创建的类数组对象;
- 包含所有传入的实际参数;
- 有
length属性,可通过索引访问(如arguments[0]); - 但不是真正的数组,不能使用
.reduce()、.map()等方法。
“类数组有长度属性,可以使用索引访问,可以 for 循环遍历,但不能使用数组的方法”。
二、陷阱:为什么 arguments.reduce 会报错?
在 1.js 中,有这样一段注释掉的代码:
// return arguments.reduce((prev, cur) => prev + cur, 0); // 报错 is not a function
为什么会报错?
因为 arguments 的类型是 [object Arguments],而非 [object Array]:
console.log(Object.prototype.toString.call(arguments)); // "[object Arguments]"
console.log(Object.prototype.toString.call([1,2,3])); // "[object Array]"
它的原型链是:
arguments → Object.prototype(特殊内部原型)→ null
而不是:
数组 → Array.prototype → Object.prototype → null
所以,arguments 没有继承 Array.prototype 上的方法。
✅ 如何把 arguments 变成真数组?
readme.md 给出了答案:
const args = [...arguments]; // ES6 扩展运算符
// 或
const args = Array.from(arguments);
// 或(ES5)
const args = Array.prototype.slice.call(arguments);
转换后,就可以安全使用 .reduce() 了:
function add2() {
const args = [...arguments];
return args.reduce((a, b) => a + b, 0);
}
三、进阶:手写 new 操作符
理解了 arguments,我们就能处理动态参数,进而挑战更高级的任务——手动实现 new。
🌟 new 到底做了什么?
当你写 new Person('张三', 18) 时,JavaScript 引擎实际执行了四步:
- 创建一个新空对象;
- 将该对象的
[[Prototype]]指向Person.prototype; - 以新对象为
this,执行Person('张三', 18); - 如果构造函数返回非
null对象,则返回它;否则返回新对象。
✅ 手写 objectFactory(基于你的思路)
利用 arguments 和 apply,我们可以还原这一过程:
function objectFactory() {
// 1. 取出构造函数(第一个参数)
const Constructor = [].shift.call(arguments);
// 2. 创建新对象,并设置其原型为 Constructor.prototype
const obj = Object.create(Constructor.prototype);
// 3. 调用构造函数,绑定 this 到 obj,传入剩余参数
const result = Constructor.apply(obj, arguments);
// 4. 处理返回值
if (typeof result === 'object' && result !== null) {
return result;
}
return obj;
}
关键点解析:
[].shift.call(arguments):巧妙借用数组方法,从arguments中分离出构造函数;Object.create(Constructor.prototype):标准方式建立原型链,避免使用非规范的__proto__;Constructor.apply(obj, arguments):apply原生支持类数组,无需转换!
⚠️ 注意:必须先设置原型,再调用构造函数,否则构造函数内若调用原型方法会失败。
四、验证:我们的 objectFactory 是否等价于 new?
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.sayHi = function() {
console.log(`你好,我是${this.name}`);
};
const p1 = new Person('李四', 20);
const p2 = objectFactory(Person, '李四', 20);
console.log(p2.name); // "李四"
p2.sayHi(); // "你好,我是李四"
console.log(p2 instanceof Person); // true
✅ 完全一致!
五、总结与最佳实践
| 概念 | 要点 |
|---|---|
arguments | 类数组对象,用于获取函数所有实参;不是数组,无数组方法 |
| 类数组转真数组 | 推荐 [...arguments] 或 Array.from(arguments) |
apply 的威力 | 可接受类数组作为参数,完美配合 arguments |
手写 new | 核心四步:创建对象 → 设置原型 → 绑定 this 执行 → 处理返回值 |
| 现代替代方案 | 新项目优先使用 剩余参数 ...args,语义更清晰 |
“
new实例化的过程……this指向新创建的对象,空对象的__proto__指向构造函数的prototype”。
掌握这些机制,你不仅能写出健壮的工具函数,更能深入理解 JavaScript 基于原型的面向对象本质。而这一切,都始于那个看似简单的 add 函数。
希望这篇文章帮你打通了从基础语法到底层机制的理解链条。如有疑问,欢迎继续探讨!😊