深入理解 JavaScript:从 `arguments` 到手写 `new`

46 阅读3分钟

深入理解 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]"

它的原型链是:

argumentsObject.prototype(特殊内部原型)→ null

而不是:

数组 → Array.prototypeObject.prototypenull

所以,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 引擎实际执行了四步:

  1. 创建一个新空对象;
  2. 将该对象的 [[Prototype]] 指向 Person.prototype
  3. 以新对象为 this,执行 Person('张三', 18)
  4. 如果构造函数返回非 null 对象,则返回它;否则返回新对象。

✅ 手写 objectFactory(基于你的思路)

利用 argumentsapply,我们可以还原这一过程:

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 函数。


希望这篇文章帮你打通了从基础语法到底层机制的理解链条。如有疑问,欢迎继续探讨!😊