手写 new 的本质与两种实现:从 Arguments 到构造流程的完整拆解

60 阅读7分钟

手写 new 的本质与两种实现:从 Arguments 到构造流程的完整拆解

在 JavaScript 的世界里,new 是一个神奇而“神秘”的关键词。
当你敲下 new Person('张三', 18) 的那一刻,表面上只是“创建一个对象”,但底层却发生了一系列严谨而精细的操作:
创建对象、绑定原型、指定 this、执行构造函数……最后再把对象返回。

理解 new,相当于真正理解 JS 面向对象体系的核心:构造函数 + 原型链 + this 绑定

为什么我们需要学习手写 new?这不仅是一道面试题,更是一把通往 JavaScript 核心机制的钥匙。要掌握它,我们得先从一个常被忽视却至关重要的角色说起——arguments。正是这个函数内部的“隐形参数集合”,为我们实现动态调用提供了可能。随后,我们将从最朴素的模拟雏形出发,逐步构建出第一种完整、稳健的手写 new 实现;再进一步,借助 argumentsshift 的巧妙组合,探索第二种更贴近底层逻辑的进阶写法。通过对比这两种方式,我们不仅能看清 new 的运行本质,还能深入理解 JavaScript 对象系统的构造规律。最终你会发现:当你真正理解了 new,也就真正理解了 JavaScript 是如何“造物”的。

一、为什么要学习 new?

学习 new 并不是为了“面试”,而是为了彻底理解 JavaScript 的对象系统

new 涵盖 JS 的三大本质:

  1. 构造函数如何工作
  2. 原型链如何建立
  3. this 如何绑定

学习手写 new,会让你真正理解下面这段代码到底发生了什么:

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

如果把 JavaScript 比作厨房,new 就像是“做菜的整个流程”:
准备食材(创建对象)、把食材摆在厨台上(this 指向)、放调味料(构造函数执行)、最后端盘(返回对象)。

理解 new,你就理解了 JS 如何“做对象”。


二、理解 new 之前必须理解 arguments

1. arguments 是什么?

arguments 是 函数运行时的一个类数组对象,表示传进来的所有参数。

我们有这样一个例子:

function add() {
   let result = 0;
   for (let i = 0; i < arguments.length; i++) {
       result += arguments[i];  
   }
   return result;
}

console.log(add(1,2));
console.log(add(1,2,3));

运行后:

3
6

很明显,即使没有在函数定义中写参数,仍然可以通过 arguments 获取传入的所有值。


2. arguments 有哪些“类数组”特征?

arguments 像数组,但不是数组,它具有:

  • 有 length
  • 可以用索引访问,如 arguments[0]
  • 但不能用数组方法:map、reduce、join

例如: 当我们对arguments调用reduce方法时

console.log(arguments.reduce(((prev,cur)=> prev + cur),0));

得到的结果是:

image.png

它就像一个“冒牌数组”,看起来很像,但没有正式的“身份证”。

例如:

console.log(Object.prototype.toString.call(arguments)); 
console.log(Object.prototype.toString.call([1,2,3]));

输出:

[object Arguments]
[object Array]

这就是“arguments 是类数组,不是数组”的证据。


3. 为什么要介绍 arguments?

因为——
在模拟 new 的过程中,arguments 是关键角色。

比如:

function Person (name,age){
        this.name = name;
        this.age = age;
    }
    function objectFactory(Constructor,...args){
        console.log(arguments[0]);
        console.log(...args);
    }  
       objectFactory(Person,'郑志鹏',18);

这里 Person 是构造函数,后面是其参数。

在模拟 new 的函数内部,我们需要:

  • 获取构造函数(arguments[0])
  • 获取构造函数的参数(arguments[1]…arguments[n])

image.png

所以 arguments 是“手写 new 必须掌握的第一块积木”。


三、用arguments初窥“模拟 new”的雏形

给出一个最粗糙的 new 模拟:

function objectFactory(){
    var obj = new Object();
    console.log(arguments,arguments[0]);
    return obj;
}

如果我们调用:

let zzp = objectFactory(Person,'郑志鹏',18);

输出了:

image.png

也就是说:

  • arguments[0] 是 Person 构造函数
  • arguments[1]、arguments[2] 是构造参数

这告诉我们:
手写 new 时,我们可以用 arguments 来提取构造函数和构造函数参数。

但在此版本中,还缺少关键步骤:

  • 这只是返回了一个普通对象,没有给它绑定原型
  • 也没有执行构造函数
  • 更没有把 this 和 obj 绑定起来

就好像一个“没灵魂的躯壳”,只是一个空对象而已。

下一步我们将正式进入真正的 new 操作流程。


四、第一种手写 new 的完整实现(核心版)

现在进入重头戏——
new 的核心实现。

给出一个极其标准且清晰的手写 new:

function objectFactory(Constructor,...args){
    var obj = new Object();     // 1. 创建空对象
    obj.__proto__ = Constructor.prototype;  // 2. 原型绑定
    Constructor.apply(obj,args) // 3. this 绑定并执行构造函数
    return obj;                 // 4. 返回对象
}
function Person (name,age){
        this.name = name;
        this.age = age;
    }
    Person.prototype.species = '人类';
    Person.prototype.sayHi = function(){
        console.log(`你好,我是${this.name}`);
    }
    let zzp = objectFactory(Person,'郑志鹏',18);
    console.log(zzp.age,zzp.species);

我们逐行理解。


步骤 1:创建空对象

var obj = new Object();

就像把一个“空盘子”端到你面前。


步骤 2:绑定原型(继承发生的地方)

obj.__proto__ = Constructor.prototype;

这是关键步骤!

new 做的事情之一就是让新对象可以访问构造函数的原型:

p.sayHi();

如果不绑定原型,这些方法就无法访问。

对象继承的能力就像是孩子继承了父亲的工具箱。
这行代码就是把工具箱放到了新对象的身上。


步骤 3:执行构造函数,让 this 指向新对象

Constructor.apply(obj,args);

这一步相当于:

obj.name = name;
obj.age = age;

构造函数内部所有挂在 this 上的属性,都会加到 obj 上。


步骤 4:返回新对象

return obj;

全部流程完成。


效果验证

 console.log(zzp.age,zzp.species);

输出:

18
人类

这说明:

  • 构造函数执行成功(age 存在)
  • 原型挂载成功(species 存在)

到这里,你已经写出了一个 可用的 new 的实现


五、第二种手写 new(进阶版:shift + arguments)

第二种实现更“灵活”,也更“JS 味”:

function objectFactory(){
    var obj = new Object();
    var Constructor = [].shift.call(arguments);
    Constructor.apply(obj,arguments);
    obj.__proto__ = Constructor.prototype;
    return obj;
}

相比上一版,它做了三件更高级的事:


1. 使用 shift 提取构造函数

var Constructor = [].shift.call(arguments);

类似于:

var Constructor = arguments[0];
arguments = ['郑志鹏', 18];

也就是说:

  • arguments[0] 是构造函数
  • 剩下的是构造参数

将 arguments 修改后,后续 apply 变得更自然:

Constructor.apply(obj, arguments);

就像自动帮你“切掉第一个元素”,简化逻辑。


2. 先 apply 再绑定原型(顺序不同但效果一致)

在上一版中,顺序是:

先绑原型 → 再执行构造函数

在这里顺序是:

先执行构造函数 → 再绑原型

对于大多数构造函数来说,这 不影响最终效果

顺序不同的原因是:
本版手写 new 更注重代码结构简洁。


3. 少了展开语法的依赖

上一版使用:

...args

而本版直接使用 arguments,不需要扩展语法。
这让它更接近“原始 JS 写法”。


效果验证

let zzp = objectFactory(Person,'郑志鹏',18);
console.log(zzp.age,zzp.species);

输出与上一版一致:

18
人类

功能仍然完整。


六、对比两种手写 new,理解底层规律

特点第一种第二种
参数处理使用剩余参数(...args)使用 arguments + shift
代码风格清晰、现代灵活、偏底层
原型绑定顺序apply 前apply 后
可读性更易读更“技巧性”

核心都一样:

  1. 创建空对象
  2. 绑定原型
  3. 构造函数执行,绑定 this
  4. 返回对象

顺序略有差别,但大体一致。


七、结语:当你理解了 new,你也理解了 JavaScript 的对象系统

手写 new 并不是为了机械记忆代码,而是为了真正理解:

  • 为什么对象能访问原型上的方法?
  • 为什么构造函数里的 this 指向实例?
  • 为什么 new 会返回一个对象?

当你意识到:

new 不是“语法糖”,而是一套流程

你的 JS 面向对象知识将会建立扎实的基础。

你可以自己实现 new,你就能理解 JavaScript 对象系统的灵魂。