手写 new:从 ES5 到 ES6,带你轻松理解 JavaScript 的实例化过程

36 阅读4分钟

在 JavaScript 中,new 是一个非常常见的操作符,用于通过构造函数创建对象实例。但你是否想过,当我们执行 new Person('张三', 18) 时,JavaScript 引擎到底做了些什么?今天我们就来一起“手写”一个 new,深入浅出地理解它的内部机制,并顺便聊聊一个常被误解的“类数组”——arguments


一、new 到底干了什么?

当你使用 new 调用一个构造函数时,JS 引擎会自动完成以下几步:

  1. 创建一个空对象
  2. 将这个空对象的 __proto__ 指向构造函数的 prototype(建立原型链)
  3. 将构造函数内部的 this 绑定到这个新对象,并执行构造函数(传入参数)
  4. 返回这个新对象

听起来是不是有点抽象?别急,我们直接动手实现一个 objectFactory 函数,来模拟 new 的行为!


二、ES5 写法:借助 arguments

先来看一段基于 ES5 风格的实现:

function objectFactory() {
  var obj = new Object(); // 1. 创建一个空对象
  var Constructor = [].shift.call(arguments); // 2. 取出第一个参数作为构造函数
  //这行代码是关键
  
  console.log(Constructor, arguments);//此时Constructor即为构造函数 arguments为修改后的类数组对象

  Constructor.apply(obj, arguments); // 3. 将构造函数的 this 指向 obj,并传入剩余参数
  obj.__proto__ = Constructor.prototype; // 4. 建立原型链

  return obj; // 5. 返回新对象
}

关键点解析

  • [].shift.call(arguments)
    arguments 是一个类数组对象(有 length 和索引,但不是真正的数组),不能直接调用数组方法。
    这里我们“借用”了数组的 shift 方法:

    • shift() 会移除并返回数组的第一个元素
    • 通过 call(arguments),让 shift 作用于 arguments
    • 结果:Constructor 得到构造函数(比如 Person),而 arguments 本身也被修改,只剩下后面的参数(如 '郑老板', 18
  • Constructor.apply(obj, arguments)
    apply 的第二个参数可以是数组或类数组,内部会自动处理。
    这一步相当于执行 Person.call(obj, '郑老板', 18),把 this 绑定到 obj,完成属性赋值。

  • obj.__proto__ = Constructor.prototype
    手动建立原型链,让新对象能访问构造函数原型上的方法(如 sayHi)和属性(如 species)。

✅ 测试一下:

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 zzp = objectFactory(Person, '郑老板', 18);

console.log(zzp.age, zzp.species); // 18 "人类"

完全符合预期!


三、ES6 写法:用 ...args 更简洁

ES6 引入了剩余参数(Rest Parameters) ,让代码更清晰:

function objectFactory(Constructor, ...args) {
  const obj = new Object(); // 创建空对象
  obj.__proto__ = Constructor.prototype; // 建立原型链
  Constructor.apply(obj, args); // 执行构造函数,this 指向 obj
  return obj;
}

优势在哪?

  • ...args 直接接收剩余参数为真数组,无需处理 arguments
  • 代码更直观:第一个参数是构造函数,后面全是参数
  • 避免了对 arguments 的“黑魔法”操作(比如 [].shift.call

两种写法本质相同,只是语法糖不同。你可以根据项目环境选择使用。


四、插曲:arguments 到底是什么?

在上面的 ES5 写法中,我们频繁用到了 arguments。那它究竟是什么?

arguments 是“类数组”

  • ✅ 有 length 属性
  • ✅ 可以用 arguments[i] 访问元素
  • 不是数组!不能用 mapreducejoin 等数组方法

举个例子:

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

console.log(add(1, 2, 3)); // 6
console.log(add(2, 3, 4, 5, 6)); // 20

看起来像数组,但:

// TypeError: arguments.reduce is not a function
function isArray() {
  return arguments.reduce((prev, cur) => prev + cur, 0);
}

如何验证它不是数组?

  • arguments.__proto__ !== Array.prototype
  • JSON.stringify(arguments) 输出的是对象形式:{&#34;0&#34;:1,&#34;1&#34;:2,&#34;2&#34;:3}
  • 而真正的数组是:[1,2,3]

如何把它变成真数组?

最简单的方式:展开运算符

function changeArray() {
  const args = [...arguments]; // 展开后包裹成数组
  console.log(args, Object.prototype.toString.call(args), args instanceof Array);
  // [1, 2, 3] &#34;[object Array]&#34; true
}
changeArray(1, 2, 3);

五、总结:手写 new 的核心逻辑

无论 ES5 还是 ES6,手写 new 的核心就四步:

  1. 创建空对象
  2. 绑定原型链obj.__proto__ = Constructor.prototype
  3. 执行构造函数 this绑定 传入参数Constructor.apply(obj, args)
  4. 返回对象

arguments 虽然方便,但要注意它只是“长得像数组”,实际是函数内部的特殊对象。在现代开发中,推荐使用 剩余参数 ...args 来替代它,代码更安全、更清晰。


最后,希望这篇文章能帮你轻松掌握 new 的本质,不再对 arguments 感到困惑。如果你觉得有用,欢迎点赞、收藏,也欢迎在评论区分享你的手写 new 心得!

📌 记住:JavaScript 的魅力,就在于这些看似简单的操作背后,藏着精巧的设计。