别再被 new 骗了!它的底裤我都扒干净了 👖

113 阅读7分钟

揭秘 JavaScript 中的 "造物主"—— 手写 new 的那些事儿 🚀

一、new 是什么?—— 藏在代码里的 "造物主"🤖

如果你是 JavaScript 世界的 "造物主",想批量生产 "对象居民",那new就是你最趁手的工具!简单说,new是 JS 里的实例化运算符,专门用来从 "构造函数蓝图" 里造出具体的对象实例。

比如你定义了一个Person构造函数(相当于人类设计图),用new Person()一敲,一个活生生的 "人对象" 就诞生了。它就像 3D 打印机,输入设计图,输出实体模型,神奇吧?

二、日常使用 new:实例化的 "四步曲"👣

平时我们写let p = new Person('张三', 18)时,JS 引擎其实偷偷干了四件大事:

  1. 造空壳:先创建一个空对象{}(相当于刚出生的空白娃娃)
  2. 绑 this:让构造函数里的this指向这个空对象(告诉娃娃:"你的属性要存在这里哦")
  3. 连家谱:把空对象的__proto__指向构造函数的prototype(给娃娃上户口,认祖归宗)
  4. 送回家:默认返回这个加工好的对象(新鲜出炉的实例到手啦)

看看代码里的例子:

javascript

function Person(name, age) {
  this.name = name; // this指向新对象,给对象加name属性
  this.age = age;   // 再加age属性
}
Person.prototype.species = '人类'; // 原型上的"家族属性"
let p = new Person('张三', 18);
console.log(p.name); // 张三(实例自己的属性)
console.log(p.species); // 人类(从原型链继承的属性)

这就是new的魔法:让对象既有自己的 "个性",又能继承 "家族共性"🔗

三、手写 myNew:山寨版 "造物主" 上线💻

知道了new的原理,我们也能自己造一个!先看第一种实现方式(用剩余参数...args):

javascript

// 模拟new功能的函数,Constructor是构造函数,args是参数列表
function objectFactory(Constructor, ...args) {
  // 1. 造空壳:创建空对象
  let obj = new Object(); 
  // 2. 绑this:让构造函数的this指向obj,并执行构造函数
  Constructor.apply(obj, args); 
  // 3. 连家谱:让obj的原型指向构造函数的原型
  obj.__proto__ = Constructor.prototype; 
  // 4. 送回家:返回创建好的对象
  return obj; 
}

认清楚各个角色:

  • Constructor:就是你要实例化的构造函数(比如 PersonCar 这类 “蓝图函数”);
  • obj:我们手动创建的空对象(new Object() 生成的那个 “空白壳子”);
  • apply:函数的内置方法,核心作用是改变函数执行时 this 的指向,并执行函数;
  • args:传给构造函数的参数列表(比如 ['李四', 19])。

测试一下效果:

javascript

// 用自己写的objectFactory创建实例
let lisi = objectFactory(Person, '李四', 19);
console.log(lisi.age); // 19(成功继承构造函数里的属性)
console.log(lisi.species); // 人类(成功通过原型链找到属性)

完美复刻!这说明只要抓住 "四步曲",我们也能当 JS 的 "造物主"😎

四、类数组 Arguments:数组界的 "伪装者"🤡

在手写new的进阶版里,会遇到一个神奇的东西 ——arguments。这货长得像数组,却不是数组,堪称 JS 界的 "伪装大师"。

1. arguments 是什么?

arguments是函数运行时自动生成的参数对象,存储着传入函数的所有实参。比如:

javascript

function add() {
  console.log(arguments); // 传入的所有参数都在这里
}
add(1, 2, 3); // arguments会是{0:1, 1:2, 2:3, length:3}

不管函数定义时有没有写参数,arguments都能把实参一网打尽,简直是函数的 "小跟班"👥

2. 类数组 vs 真数组:真假美猴王

arguments是典型的类数组,它和真数组的区别就像 "高仿鞋" 和 "正品鞋"—— 看着像,功能差很多:

特点类数组 arguments真数组 [1,2,3]
长度有 length 属性有 length 属性
索引可通过下标访问(如 arguments [0])可通过下标访问
数组方法没有!不能用 reduce、map、join 等有全套数组方法

看代码里的坑:

javascript

function add() {
  // 报错!arguments没有reduce方法
  return arguments.reduce((prev, cur) => prev + cur, 0); 
}

认清楚各个角色:

  • reduce:数组的归并方法,遍历数组,把所有元素累加成一个值,只有真数组可以调用。
  • (prev, cur) => prev + cur:reduce的回调函数,负责每次累加,其中:prev是上一次累加的结果(初始时是后面的0);cur是当前遍历到的数组元素;prev + cur是把累加值和当前值相加,作为下一次的prev

这就是类数组的 "坑":别被它的外表骗了,它真的不是数组!

3. 类数组转真数组:"脱胎换骨" 术

想让arguments用上数组方法?得先给它 "换身衣服",变成真数组。常用的三种方法:

  1. 扩展运算符[...arguments](最简单直接)
  2. Array.fromArray.from(arguments)(ES6 新增,万能转换)
  3. 数组借方法Array.prototype.slice.call(arguments)(老派但好用)

代码示例:

javascript

function add() {
  // 转换为真数组
  const args = [...arguments]; 
  // 这下能用reduce了
  return args.reduce((prev, cur) => prev + cur, 0); 
}
console.log(add(1,2,3)); // 6(成功求和)

转换后,类数组就成了真正的数组,再也不用怕 "方法缺失" 的尴尬了😌

五、用 arguments 实现 new:进阶版 "造物主工具"🔧

知道了arguments的用法,我们可以写一个更灵活的new模拟器(不用剩余参数,兼容更多场景):

javascript

function objectFactory() {
  // 1. 造空壳
  let obj = new Object(); 
  // 2. 从arguments里取出构造函数(第一个参数)
  // 因为arguments是类数组,借数组的shift方法"移除并返回第一个元素"
  let Constructor = [].shift.call(arguments); 
  // 3. 绑this:用剩下的arguments作为参数,执行构造函数
  Constructor.apply(obj, arguments); 
  // 4. 连家谱
  obj.__proto__ = Constructor.prototype; 
  // 5. 送回家
  return obj; 
}

搞明白:

1、[].shift.call(arguments) 是对 arguments 这个类数组做了局部的 “数组式操作”  —— 借数组的 shift 方法操作类数组,而非把整个 arguments 转成真数组。咱们一步步拆解清楚。这行代码的本质是  “数组方法借调” ,不是 “类数组转真数组”,拆解成 3 层理解:

  • []:创建一个空的真数组,目的是 “借” 它原型上的 shift 方法;
  • .shift:数组的 shift 方法,作用是 “删除并返回数组的第一个元素”;
  • .call(arguments):改变 shift 方法的 this 指向,让它操作 arguments 而非空数组。

2、你可能会问:apply 的第二个参数要求是 “数组 / 类数组”,这里传没转成真数组的 arguments 为啥不报错?

因为 apply 的设计本身就兼容类数组apply 对第二个参数的要求是:“只要有 length 属性、能通过下标访问元素” 即可(类数组完全满足),不需要是严格的真数组。

测试一下:

javascript

let wangwu = objectFactory(Person, '王五', 20);
console.log(wangwu.name); // 王五(完美运行)

这种方式更灵活,不用提前声明参数,全靠arguments动态获取,是不是很秀?✨

六、面试官想考察什么?—— 透过现象看本质🔍

当面试官让你手写new时,他可不是想看你默写代码,而是在考察:

  1. 对原型链的理解:知不知道__proto__prototype的关系(这是 JS 面向对象的核心)
  2. 对 this 绑定的掌握:能不能说清apply如何改变this指向
  3. 对类数组的认知:是否了解arguments的特性及转换方法
  4. 底层思维能力:会不会把复杂功能拆解成步骤(比如 "四步曲")

说白了,这道题是 "JS 基础综合体检",能看出你是不是真的理解 JS,而不是只会调用 API。

七、总结:从 "会用" 到 "懂原理" 的跨越🚀

今天我们从new的功能聊到实现,从类数组arguments的特性讲到转换,其实核心就一个:知其然,更要知其所以然

new看似简单,背后藏着原型链、this 绑定的深层逻辑;arguments看似普通,却能帮我们理解 JS 的动态特性。当你能亲手实现这些 "内置功能" 时,就说明你已经从 "API 调用者" 升级成 "原理探索者" 了。

最后送大家一句:JS 的世界里,没有魔法,只有没看透的原理。下次再遇到new,不妨想想它背后的 "四步曲",说不定会有新发现!😉