手写 `new` 运算符:从底层原理到完整实现(含 Arguments 深度解析)

37 阅读3分钟

手写 new 运算符:从底层原理到完整实现(含 Arguments 深度解析)

在 JavaScript 中,new 运算符是实现面向对象编程的重要基石,也是前端面试中的高频考点。它看似简单,用法极其常见,但底层实现机制却往往被忽略。这篇文章将从原理讲起,逐步带你手写一个可工作的 new 运算符模拟版本。同时,我们也会深入解析常见却容易混淆的类数组对象 Arguments


一、new 运算符到底做了什么?

当你执行:

let p = new Person('张三', 18);

JavaScript 底层大约会依次完成以下步骤:

  1. 创建一个全新的空对象

    const obj = {};
    
  2. 将这个对象的原型指向构造函数的 prototype

    obj.__proto__ = Person.prototype;
    
  3. 以新对象作为上下文(this),执行构造函数

    Person.apply(obj, arguments);
    
  4. 返回这个对象

    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]
  • 不能使用数组方法,如 mapreducejoin

示例:

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数组
类型类数组真数组
原型ArgumentsArray
是否可用数组方法

因此,下面写法会报错:

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 转数组的多种方法