手写 new:理解 JavaScript 中对象创建的本质
在 JavaScript 的面向对象编程中,new 操作符是我们最常使用的工具之一。它看似简单,背后却隐藏着一套完整的对象创建机制。今天,我们就来手写一个模拟 new 的函数,并在这个过程中,顺便搞清楚一个经常被忽视但极其重要的概念——类数组对象 arguments。
一、new 到底做了什么?
当我们执行如下代码:
function Person(name) {
this.name = name;
}
const p = new Person('Alice');
new 实际上完成了四件事:
- 创建一个全新的空对象;
- 将这个新对象的
__proto__(即内部原型)指向构造函数的prototype; - 将构造函数内部的
this绑定到这个新对象,并执行构造函数; - 如果构造函数没有显式返回一个对象,则返回新创建的对象。
二、方案一:现代写法(兼容 ES5)
我们可以用一个函数来模拟上述过程:
function objectFactory(Constructor,...args){
//var obj ={};
var obj = new Object();//从空对象开始
Constructor.apply(obj,args);//绑定this指向
obj.__proto__ = Constructor.prototype;//指向构造函数的原型对象
return obj;//返回该对象
}
关键点解析:
- 我们可以使用对象字面量或者new Object()来创建一个全新的空对象,我们这里使用了new Object(),原因是,从构造的形式上来说,通过new Object()创建的对象“血统”更加纯正;
- 通过参数传递,我们可以拿到构造函数,所以我们可以通过apply方法将构造函数的this指向obj,剩余参数通过...(rest运算符)组成一个数组传入apply。
- 通过__proto__指向构造函数的原型对象,现代更推荐使用[[prototype]]
那么现在的你是否在思考?ES6之前并没有rest运算符,我们又是怎么实现new的效果的呢,别急,答案马上揭晓...
三、插入话题:什么是 arguments?
在任意函数内部,JavaScript 引擎会自动提供一个特殊的类数组对象——arguments。它包含调用时传入的所有参数。
例如:
function add() {
console.log(arguments); //(看起来像数组,但不是!)
}
add(1, 2);
- 看起来它具有索引,并且也从0开始,我们也能够通过下标访问到数据。
- 但是当我们通过Object.prototype.toString.call()去查看类型时,显示的却是[object Arguments]而非[object Array],这也代表着它的确不是一个数组。
arguments 的特点:
- 有
length属性; - 可通过索引访问(如
arguments[0]); - 但它不是真正的数组,不能直接使用
map、reduce、join等数组方法; - 它的原型是
Object.prototype,而不是Array.prototype。
你可以验证:
console.log(arguments instanceof Array); // false
console.log(Object.prototype.toString.call(arguments)); // "[object Arguments]"
如何将 arguments 转为真数组?
常见方法有:
const args = Array.prototype.slice.call(arguments);
// 或
const args = [...arguments]; // ES6 扩展运算符(要求环境支持 iterator)
// 或
const args = Array.from(arguments);
一旦转成数组,就能自由使用所有数组方法了:
function add() {
const args = [...arguments];
return args.reduce((sum, num) => sum + num, 0);
}
console.log(add(1, 2, 3, 4)); // 10
四、方案二:兼容旧环境的 objectFactor(使用 arguments)
如果我们想在不支持 ... 扩展运算符的环境中实现 new,就需要借助 arguments:
function objectFactory(){
var obj = new Object();//从空对象开始
var Constructor = [].shift.call(arguments);//shift函数借用
Constructor.apply(obj,arguments);
obj.__proto__ = Constructor.prototype;//指向构造函数的原型对象
return obj;
}
关键点解析:
- shift函数:移除数组的第一个元素并返回,shift函数会将原数组进行修改,我们通过
[].shift.call(arguments)将shift函数借给arguemnt进行操作。 - 你或许会感到疑惑?既然前面已经说了arguments不是数组为什么还能调用数组方法?关键在于,JavaScript并不强制要求调用者一定是数组,只要结构像就行。而对于shift操作它做了什么呢?它需要知道数组长度来进行遍历,需要数组下标(键)进行操作,而arguments恰好具有这两者,于是我们可以通过.call方法将this强行指向arguments,这样就可以“骗过”这个方法,完美执行!!
- 从apply的参数传递来看,我们并不需要传递一个数组类型作为第二个参数,我们传递(arguments)也完全没有问题,这就说明apply的第二个参数并不一定是数组,只要我们将数据一次性传进就行
使用示例:
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 ls = objectFactory(Person,'李四',19);
console.log(p,ls);
console.log(ls.age,ls.species);
五、总结
new的本质是创建对象 + 绑定原型 + 执行初始化逻辑;- 手写
new帮助我们深入理解 JavaScript 的原型链和this机制; arguments是函数内部的“隐藏参数包”,虽像数组但不是数组;- 在现代开发中,优先使用
...rest参数;但在兼容旧代码时,arguments仍是重要工具; - 将
arguments转为真数组后,才能享受数组方法的便利。
六、延伸思考
你有没有想过:为什么 JavaScript 要设计 arguments 这种类数组?为什么不直接用数组?
这其实源于早期 JavaScript 的设计哲学——灵活性优先于严格类型。动态参数、可变参数数量是 JS 函数的一大特色,而 arguments 正是这一特性的基石。
如今,随着 ES6 的普及,rest 参数(...args)逐渐取代了 arguments 的大部分用途,但理解它,依然是进阶 JS 开发者的必修课。
希望这篇文章能帮你打通 new 和 arguments 的任督二脉!如果你觉得有用,欢迎点赞、收藏,也欢迎在评论区讨论更多细节 😊