手写 new 运算符:从底层原理到完整实现(含 Arguments 深度解析)
在 JavaScript 中,new 运算符是实现面向对象编程的重要基石,也是前端面试中的高频考点。它看似简单,用法极其常见,但底层实现机制却往往被忽略。这篇文章将从原理讲起,逐步带你手写一个可工作的 new 运算符模拟版本。同时,我们也会深入解析常见却容易混淆的类数组对象 Arguments。
一、new 运算符到底做了什么?
当你执行:
let p = new Person('张三', 18);
JavaScript 底层大约会依次完成以下步骤:
-
创建一个全新的空对象
const obj = {}; -
将这个对象的原型指向构造函数的
prototypeobj.__proto__ = Person.prototype; -
以新对象作为上下文(this),执行构造函数
Person.apply(obj, arguments); -
返回这个对象
return obj;
这四步构成了 new 运算符的全部核心逻辑。
二、手写一个最小可用版本的 new
基于以上行为,我们可以实现一个简单的 objectFactory:
function objectFactory() {
const obj = new Object();
const Constructor = [].shift.call(arguments);
// 执行构造函数,将 this 绑定到新对象上
Constructor.apply(obj, arguments);
// 原型链关联
obj.__proto__ = Constructor.prototype;
return obj;
}
测试:
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.species = '人类';
Person.prototype.sayHi = function() {
console.log(`你好,我是 ${this.name}`);
}
const p = objectFactory(Person, '张三', 18);
console.log(p.age); // 18
console.log(p.species); // 人类
这个版本已经具备基础的 new 行为。
三、构造函数返回对象怎么办?更严谨的版本
真正的 new 有一个重要的规则:
如果构造函数显式返回一个对象,那么最终结果是这个对象;否则返回新创建的实例对象。
因此我们可以升级为更严谨的实现:
function simulateNew() {
const obj = {};
const Constructor = [].shift.call(arguments);
obj.__proto__ = Constructor.prototype;
const result = Constructor.apply(obj, arguments);
return (typeof result === 'object' && result !== null)
? result
: obj;
}
这是面试中经常被加分的细节点。
四、类数组对象 Arguments 深度解析
arguments 是 JS 函数的一个内置对象,用来存储传入的所有实参。但它并不是一个真正的数组,而是“类数组”。
什么是类数组?
类数组同时具备以下特征:
- 拥有
length - 可以通过索引访问,如
arguments[0] - 不能使用数组方法,如
map、reduce、join等
示例:
function test() {
console.log(arguments);
console.log(arguments instanceof Array); // false
}
输出表明 arguments 不是数组。
五、Arguments 与数组的比较
让我们看一个具体示例:
function add() {
console.log(JSON.stringify(arguments));
console.log(JSON.stringify([1,2,3]));
const args = [...arguments];
console.log(args, Object.prototype.toString.call(args), args instanceof Array);
}
add(1, 2, 3);
关键差异:
| 项目 | arguments | 数组 |
|---|---|---|
| 类型 | 类数组 | 真数组 |
| 原型 | Arguments | Array |
| 是否可用数组方法 | 否 | 是 |
因此,下面写法会报错:
arguments.reduce((a, b) => a + b); // 错误
正确写法:
[...arguments].reduce((a, b) => a + b);
六、如何把 Arguments 转成数组?
常用三种方式:
1. 扩展运算符(最推荐)
const args = [...arguments];
2. Array.from
const args = Array.from(arguments);
3. call + slice
const args = [].slice.call(arguments);
转换之后就可以安全使用各类数组方法。
总结
new运算符的底层执行机制- 手写
new的基础版与进阶完整版实现 - 类数组对象 Arguments 的本质与常见误区
- Arguments 转数组的多种方法