在 JavaScript 中,new 是一个看似简单却承载着面向对象核心机制的关键字。当我们写下 new Person() 时,引擎并非只是"调用一个函数",而是在背后完成了一系列精密的初始化操作:创建对象、绑定原型、执行构造逻辑、返回实例。理解这一过程,不仅能帮助我们掌握原型继承的本质,也为实现高级框架(如 Vue 的响应式系统)打下基础。本文将从零开始,手写一个 new 的模拟函数,并深入剖析其每一步的作用。
new 到底做了什么?
使用 new 调用构造函数时,JavaScript 引擎会按以下顺序执行:
- 创建一个全新的空对象;
- 将该对象的
__proto__指向构造函数的prototype; - 将构造函数内部的
this绑定到这个新对象,并执行函数体; - 如果构造函数没有显式返回一个对象,则自动返回新创建的对象。
这四步构成了 JavaScript 原型式面向对象的基石。为了验证我们的理解,可以尝试手动复现这一过程。
手写 objectFactory 模拟 new
ini
function objectFactory() {
const obj = {};
const Constructor = Array.prototype.shift.call(arguments);
obj.__proto__ = Constructor.prototype;
Constructor.apply(obj, arguments);
return obj;
}
这段代码精炼地还原了 new 的行为。首先,通过 shift 从 arguments 中取出第一个参数(即构造函数),其余参数作为实参传递给构造函数。接着,将新对象的原型链指向构造函数的 prototype,确保实例能访问其方法。最后,用 apply 将 this 绑定到 obj 并执行构造逻辑。
测试如下:
ini
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.sayHi = function() {
console.log('你好,我是' + this.name);
};
const p1 = new Person('张三', 18);
const p2 = objectFactory(Person, '张三', 18);
console.log(p1.sayHi === p2.sayHi); // true
两个实例不仅属性一致,连原型方法也完全共享,证明我们的模拟是成功的。
关于 arguments:类数组的灵活与局限
在 objectFactory 中,我们使用了 arguments 对象来获取传入的所有参数。arguments 是函数内部的一个类数组对象 :它拥有 length 属性,可通过索引访问每个参数,但不具备数组的原生方法(如 map、reduce)。
javascript
function add() {
const args = [...arguments];
return args.reduce((sum, val) => sum + val, 0);
}
console.log(add(1, 2, 3)); // 6
通过扩展运算符 [...arguments],我们可以将其转换为真正的数组,从而使用现代数组方法。这种灵活性使得 JavaScript 函数天然支持可变参数,无需预先声明参数个数。
为什么需要手动处理 arguments?
在模拟 new 时,无法预知用户会传入多少个参数,也无法提前定义形参列表。因此,必须依赖 arguments 动态获取所有实参。而 Array.prototype.shift.call(arguments) 是一种经典技巧:它将 arguments 视为数组,从中"弹出"第一个元素(构造函数),剩余部分自然成为构造函数的参数列表。
值得注意的是,arguments 并非真正的数组,其 __proto__ 指向 Object.prototype,而非 Array.prototype。这也是为何不能直接调用 arguments.shift()------必须借助 call 或 apply 借用数组的方法。
返回值的边界情况
标准 new 运算符还有一个细节:如果构造函数显式返回一个对象,则忽略新创建的实例,直接返回该对象。例如:
javascript
function Test() {
this.value = 1;
return { value: 2 };
}
console.log(new Test().value); // 2
我们的 objectFactory 目前未处理此情况。若要完全兼容,可补充判断:
ini
const result = Constructor.apply(obj, arguments);
return (typeof result === 'object' && result !== null) ? result : obj;
但在大多数实际场景中,构造函数不返回值或仅返回基本类型,因此简化版已足够使用。
总结
手写 new 不仅是一道常见的面试题,更是深入理解 JavaScript 对象模型的关键实践。通过模拟其实例化过程,我们清晰地看到:对象的创建、原型的链接、上下文的绑定 是如何协同工作的。同时,对 arguments 的操作也展示了 JavaScript 在参数处理上的动态特性。
尽管现代开发中 class 语法已普及,但其底层依然依赖 new 和原型链。掌握这些原始机制,意味着你不仅能写出更健壮的代码,还能在阅读框架源码、调试复杂继承关系时游刃有余。毕竟,真正的 JavaScript 功力,往往藏在这些"基础却深刻"的细节之中。