在日常开发中,我们几乎每天都在使用 new 来创建对象实例,也经常在函数内部接触到神秘的 arguments 对象。但你是否真正理解它们背后发生了什么?今天我们就来彻底拆解这两个 JavaScript 经典机制,并手把手实现一个完整的 new 模拟函数,顺便解决 arguments 不能直接用数组方法的老大难问题。
一、new 到底做了什么?
在使用 new 操作符时,JavaScript 引擎悄悄做了四件事:
- 创建一个全新的空对象 {}
- 将这个空对象的 proto 指向构造函数的 prototype
- 将构造函数的 this 绑定到这个新对象上,并执行构造函数
- 如果构造函数没有显式返回对象,则自动返回这个新对象
我们用最经典的例子来看:
JavaScript
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.sayHi = function() {
console.log(`你好,我是${this.name}`);
};
const p = new Person('张三', 18);
执行 new Person('张三', 18) 后,p 就拥有了 name、age 属性,还能访问到原型上的 sayHi 方法。
二、手写一个完美的 new 模拟函数
我们来实现一个名为 objectFactory 的函数,完整模拟 new 的行为:
JavaScript
function objectFactory() {
// 1. 创建一个空对象
const obj = new Object();
// 2. 取出构造函数(第一个参数),剩余的是构造函数参数
const Constructor = [].shift.call(arguments);
// 3. 链接原型链
obj.__proto__ = Constructor.prototype;
// 4. 绑定 this 并执行构造函数
const result = Constructor.apply(obj, arguments);
// 5. 如果构造函数返回了对象,则返回该对象;否则返回我们创建的 obj
return typeof result === 'object' && result !== null ? result : obj;
}
注意:这里有一个关键点!很多人手写 new 时忽略了第5步:构造函数如果显式返回一个对象,应该以返回值为准。
验证一下我们的实现:
JavaScript
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.species = '人类';
Person.prototype.sayHi = function() {
console.log(`你好,我是${this.name}`);
};
const ggg = objectFactory(Person, 'ggg', 20);
console.log(ggg.age); // 20
console.log(ggg.species); // 人类
ggg.sayHi(); // 你好,我是ggg
完美!原型链、this 绑定、属性赋值全部正确。
进阶:处理构造函数返回对象的情况
JavaScript
function Strange() {
this.a = 1;
return { b: 2 }; // 显式返回对象
}
const s = objectFactory(Strange);
console.log(s); // { b: 2 } 而不是 { a: 1 }
这正是 new 的真实行为,我们的实现也完全一致。
三、神秘的 arguments 对象:它到底是什么?
在函数内部,arguments 是一个类数组对象,它有以下特点:
- 有 length 属性
- 可以用 arguments[0]、arguments[1] 访问参数
- 不能直接调用 map、reduce、filter 等数组方法
- 在非严格模式下,修改 arguments[i] 会影响对应的具名参数(反之亦然)
JavaScript
function test(a, b) {
console.log(arguments.length); // 2
console.log(arguments[0]); // 传入的第一个参数
arguments[0] = 100;
console.log(a); // 100(非严格模式下会被修改)
}
如何把 arguments 变成真正的数组?
常见的三种方式(推荐使用扩展运算符):
JavaScript
function add() {
// 方法1:Array.from(最直观)
const args = Array.from(arguments);
// 方法2:扩展运算符(最优雅)
const args2 = [...arguments];
// 方法3:借用数组原型 slice(经典老方法)
const args3 = Array.prototype.slice.call(arguments);
// 现在可以愉快地使用数组方法了
return args.reduce((sum, cur) => sum + cur, 0);
}
console.log(add(1, 2, 3, 4, 5)); // 15
arguments 的 proto 指向哪里?
JavaScript
function fn() {
console.log(arguments.__proto__ === Object.prototype); // true
console.log(Object.prototype.toString.call(arguments)); // [object Arguments]
}
它本质上是一个普通对象,只是长得像数组。
四、实际应用场景举例
场景1:实现一个可变参数的求和函数
JavaScript
function sum() {
return [...arguments].reduce((a, b) => a + b, 0);
}
// 或者更优雅地直接使用 rest 参数(推荐)
function sumModern(...args) {
return args.reduce((a, b) => a + b, 0);
}
场景2:手写 bind 函数时需要用到 arguments
JavaScript
Function.prototype.myBind = function(context, ...bindArgs) {
const self = this;
return function(...callArgs) {
return self.apply(context, bindArgs.concat(callArgs));
};
};
五、ES6 class 的 new 还能被模拟吗?
答案是:完全可以!class 只是语法糖,本质还是原型继承:
JavaScript
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name} 发出声音`);
}
}
const dog = objectFactory(Animal, '旺财');
dog.speak(); // 旺财 发出声音
我们的 objectFactory 依然完美工作!
总结
通过今天的手写实现,我们彻底明白了:
- new 操作符的四个步骤缺一不可(尤其是很多人忽略的返回值处理)
- arguments 是一个类数组对象,通过扩展运算符或 Array.from 可以轻松转为真数组
- 手写 new 的核心是:创建对象 → 链接原型 → 绑定 this → 处理返回值
- 理解这些底层机制,能让我们写出更健壮、更灵活的 JavaScript 代码
掌握了这些,你就已经站在了无数前端工程师的前面。去试试把今天的 objectFactory 加入你的工具库吧!