面试官让我手写 new,我顺手把 arguments 的底裤都扒了

61 阅读5分钟

手写 new:理解 JavaScript 中对象创建的本质

在 JavaScript 的面向对象编程中,new 操作符是我们最常使用的工具之一。它看似简单,背后却隐藏着一套完整的对象创建机制。今天,我们就来手写一个模拟 new 的函数,并在这个过程中,顺便搞清楚一个经常被忽视但极其重要的概念——类数组对象 arguments


一、new 到底做了什么?

当我们执行如下代码:

function Person(name) {
  this.name = name;
}
const p = new Person('Alice');

new 实际上完成了四件事:

  1. 创建一个全新的空对象;
  2. 将这个新对象的 __proto__(即内部原型)指向构造函数的 prototype
  3. 将构造函数内部的 this 绑定到这个新对象,并执行构造函数;
  4. 如果构造函数没有显式返回一个对象,则返回新创建的对象。

二、方案一:现代写法(兼容 ES5)

我们可以用一个函数来模拟上述过程:

function objectFactory(Constructor,...args){
        //var obj ={};
       var obj = new Object();//从空对象开始
       Constructor.apply(obj,args);//绑定this指向
       obj.__proto__ = Constructor.prototype;//指向构造函数的原型对象
       return obj;//返回该对象
}

关键点解析:

  1. 我们可以使用对象字面量或者new Object()来创建一个全新的空对象,我们这里使用了new Object(),原因是,从构造的形式上来说,通过new Object()创建的对象“血统”更加纯正;
  2. 通过参数传递,我们可以拿到构造函数,所以我们可以通过apply方法将构造函数的this指向obj,剩余参数通过...(rest运算符)组成一个数组传入apply。
  3. 通过__proto__指向构造函数的原型对象,现代更推荐使用[[prototype]]

那么现在的你是否在思考?ES6之前并没有rest运算符,我们又是怎么实现new的效果的呢,别急,答案马上揭晓...

三、插入话题:什么是 arguments

在任意函数内部,JavaScript 引擎会自动提供一个特殊的类数组对象——arguments。它包含调用时传入的所有参数。

例如:

function add() {
  console.log(arguments); //(看起来像数组,但不是!)
}
add(1, 2);

image.png

  • 看起来它具有索引,并且也从0开始,我们也能够通过下标访问到数据。
  • 但是当我们通过Object.prototype.toString.call()去查看类型时,显示的却是[object Arguments]而非[object Array],这也代表着它的确不是一个数组。

arguments 的特点:

  • length 属性;
  • 可通过索引访问(如 arguments[0]);
  • 但它不是真正的数组,不能直接使用 mapreducejoin 等数组方法;
  • 它的原型是 Object.prototype,而不是 Array.prototype

你可以验证:

console.log(arguments instanceof Array); // false
console.log(Object.prototype.toString.call(arguments)); // "[object Arguments]"

如何将 arguments 转为真数组?

常见方法有:

const args = Array.prototype.slice.call(arguments);
// 或
const args = [...arguments]; // ES6 扩展运算符(要求环境支持 iterator)
// 或
const args = Array.from(arguments);

一旦转成数组,就能自由使用所有数组方法了:

function add() {
  const args = [...arguments];
  return args.reduce((sum, num) => sum + num, 0);
}
console.log(add(1, 2, 3, 4)); // 10

四、方案二:兼容旧环境的 objectFactor(使用 arguments

如果我们想在不支持 ... 扩展运算符的环境中实现 new,就需要借助 arguments

function objectFactory(){
            
    var obj = new Object();//从空对象开始
    var Constructor = [].shift.call(arguments);//shift函数借用      
    Constructor.apply(obj,arguments);
    obj.__proto__ = Constructor.prototype;//指向构造函数的原型对象
           
    return obj;
}

关键点解析:

  1. shift函数:移除数组的第一个元素并返回,shift函数会将原数组进行修改,我们通过[].shift.call(arguments)将shift函数借给arguemnt进行操作。
  2. 你或许会感到疑惑?既然前面已经说了arguments不是数组为什么还能调用数组方法?关键在于,JavaScript并不强制要求调用者一定是数组,只要结构像就行。而对于shift操作它做了什么呢?它需要知道数组长度来进行遍历,需要数组下标(键)进行操作,而arguments恰好具有这两者,于是我们可以通过.call方法将this强行指向arguments,这样就可以“骗过”这个方法,完美执行!!
  3. 从apply的参数传递来看,我们并不需要传递一个数组类型作为第二个参数,我们传递(arguments)也完全没有问题,这就说明apply的第二个参数并不一定是数组,只要我们将数据一次性传进就行

使用示例:

function Person(name,age){
            this.name = name;
            this.age = age;
        }
        Person.prototype.species = '人';
        Person.prototype.sayHi = function(){
            console.log(`你好,我是${this.name}`);
        }
        let p = new Person('张三',18);
        let ls = objectFactory(Person,'李四',19);
        console.log(p,ls);
        console.log(ls.age,ls.species);

image.png


五、总结

  • new 的本质是创建对象 + 绑定原型 + 执行初始化逻辑
  • 手写 new 帮助我们深入理解 JavaScript 的原型链和 this 机制;
  • arguments 是函数内部的“隐藏参数包”,虽像数组但不是数组;
  • 在现代开发中,优先使用 ...rest 参数;但在兼容旧代码时,arguments 仍是重要工具;
  • arguments 转为真数组后,才能享受数组方法的便利。

六、延伸思考

你有没有想过:为什么 JavaScript 要设计 arguments 这种类数组?为什么不直接用数组?

这其实源于早期 JavaScript 的设计哲学——灵活性优先于严格类型。动态参数、可变参数数量是 JS 函数的一大特色,而 arguments 正是这一特性的基石。

如今,随着 ES6 的普及,rest 参数(...args)逐渐取代了 arguments 的大部分用途,但理解它,依然是进阶 JS 开发者的必修课。


希望这篇文章能帮你打通 newarguments 的任督二脉!如果你觉得有用,欢迎点赞、收藏,也欢迎在评论区讨论更多细节 😊