在 JavaScript 中,new 是一个非常常见的操作符,用于通过构造函数创建对象实例。但你是否想过,当我们执行 new Person('张三', 18) 时,JavaScript 引擎到底做了些什么?今天我们就来一起“手写”一个 new,深入浅出地理解它的内部机制,并顺便聊聊一个常被误解的“类数组”——arguments。
一、new 到底干了什么?
当你使用 new 调用一个构造函数时,JS 引擎会自动完成以下几步:
- 创建一个空对象
- 将这个空对象的
__proto__指向构造函数的prototype(建立原型链) - 将构造函数内部的
this绑定到这个新对象,并执行构造函数(传入参数) - 返回这个新对象
听起来是不是有点抽象?别急,我们直接动手实现一个 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]访问元素 - ❌ 不是数组!不能用
map、reduce、join等数组方法
举个例子:
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.prototypeJSON.stringify(arguments)输出的是对象形式:{"0":1,"1":2,"2":3}- 而真正的数组是:
[1,2,3]
如何把它变成真数组?
最简单的方式:展开运算符!
function changeArray() {
const args = [...arguments]; // 展开后包裹成数组
console.log(args, Object.prototype.toString.call(args), args instanceof Array);
// [1, 2, 3] "[object Array]" true
}
changeArray(1, 2, 3);
五、总结:手写 new 的核心逻辑
无论 ES5 还是 ES6,手写 new 的核心就四步:
- 创建空对象
- 绑定原型链(
obj.__proto__ = Constructor.prototype) - 执行构造函数 this绑定 传入参数(
Constructor.apply(obj, args)) - 返回对象
而 arguments 虽然方便,但要注意它只是“长得像数组”,实际是函数内部的特殊对象。在现代开发中,推荐使用 剩余参数 ...args 来替代它,代码更安全、更清晰。
最后,希望这篇文章能帮你轻松掌握 new 的本质,不再对 arguments 感到困惑。如果你觉得有用,欢迎点赞、收藏,也欢迎在评论区分享你的手写 new 心得!
📌 记住:JavaScript 的魅力,就在于这些看似简单的操作背后,藏着精巧的设计。