嗨,各位未来的 大佬们!👋
在 的世界里, 关键字简直就是创造对象的“魔法棒”。每次我们写下 的时候,一个全新的、拥有特定属性和方法的对象就诞生了。但你有没有好奇过,这短短的三个字母背后,到底发生了什么?它又是如何把一个普普通通的函数(构造函数)变成一个对象的蓝图的呢?
今天,咱们就来一次硬核挑战:手写实现 的功能! 这不仅能让你对 的原型链和面向对象有更深层次的理解,还能让你在面试中秀出真正的技术肌肉!💪
一、 运算符:实例化的过程 🛠️
在开始“造轮子”之前,咱们先快速回顾一下,当你使用 时, 引擎在幕后默默完成了哪几个关键步骤?
记住这个**“四步走”**战略:
1. 创建空对象():
首先,JS 会创建一个全新的、普通的 JavaScript 空对象var obj = {} 或 var obj = new Object()。
2. 设置原型链Prototype Link:
新创建的对象的 proto 属性会被链接到构造函数的 prototype 对象上。这是实现继承的关键!这样,新对象就能访问构造函数原型上的所有属性和方法了。
obj.__proto__ = Constructor.prototype
3. 绑定 并执行构造函Execute:
将这个新对象作为构造函数Constructor内部的 this。然后,执行构造函数内部的代码,为这个新对象添加属性和方法。
4. 返回对象() :
-
如果构造函数没有显式返回一个对象,那么 表达式会默认返回这个新创建的对象(即 指向的那个对象)。
-
如果构造函数显式返回了一个对象(非原始值),那么 表达式会返回这个被返回的对象。
💡 核心思想: 就是一个语法糖,它帮我们自动完成了从“创建空对象”到“设置原型”再到“执行初始化代码”的一系列步骤。
二、手写 🏭
现在,咱们就用一个普通的函数 来模拟实现 的功能。目标是让 达到和 一样的效果。
1. 初版实现(使用 运算符)
为了方便,我们先使用 的 运算符 来收集除第一个参数(构造函数)之外的所有参数。
JavaScript
/**
* 模拟 new 运算符
* @param {Function} Constructor 构造函数
* @param {...any} args 传递给构造函数的参数
* @returns {object} 新创建的对象实例
*/
function objectFactory(Constructor, ...args) {
// 1. 创建一个空对象
// Object.create() 是一个更推荐的方式,因为它能直接设置原型
// 但为了贴合 new 的原理,我们先用 new Object()
const obj = new Object();
// 2. 设置原型链:将新对象的原型链接到构造函数的原型
// 记住:这是实现继承和方法共享的关键!
obj.__proto__ = Constructor.prototype;
// 3. 绑定 this 并执行构造函数
// apply/call 可以改变函数执行时的 this 指向
// 我们将 obj 设为 this,并传递参数 args
const result = Constructor.apply(obj, args);
// 4. 返回对象:处理构造函数返回值
// 如果构造函数显式返回了一个对象,则返回该对象,否则返回新创建的 obj
// 检查 result 是否是对象(非 null)且不是原始值
const isObject = typeof result === 'object' && result !== null;
const isFunction = typeof result === 'function';
if (isObject || isFunction) {
return result; // 返回构造函数显式返回的对象
}
// 默认返回新创建的对象
return obj;
}
这段代码已经非常接近 的完整功能了!
2. 深入理解:解决 的难题(不用 )
在你的原始代码中,为了不用 的 运算符(这在一些老旧环境或为了纯粹模拟 时很有用),你巧妙地使用了 这个类数组对象。
来,咱们先把目光聚焦到这个老朋友:。
💡 类数组 的本质
对象是在函数调用时自动创建的。它具备类数组的特性:
- 它有 属性(表示传入的参数个数)。
- 你可以使用索引(如 )来访问参数。
但它不是真正的数组!它没有 上的方法,比如 、、 等,这也是为什么你的代码中 会报 。
运行一下Object.prototype.toString.call(argumwnts)可以看到它的实际类型是Arguments对象:
3. 终极 版本(你代码的精髓)
在不使用 的情况下,我们需要手动从 中分离出构造函数 和参数 。
JavaScript
function objectFactory() {
// 1. 创建一个空对象
var obj = new Object();
// 2. 提取构造函数(它是 arguments 的第一个元素)
// 技巧:使用 Array.prototype.shift.call(arguments)
// 解释:[] 只是用来“借用” Array.prototype 上的 shift 方法。
// shift 方法会移除并返回数组的第一个元素,同时修改原数组。
// 这里的 arguments 被当作 this 传入,所以它被修改了。
var Constructor = [].shift.call(arguments);
// 3. 绑定 this 并执行构造函数
// 此时 arguments 剩下的元素就是构造函数需要的参数
// apply/call 在传入类数组时,可以正确地把它们当作参数列表展开。
var result = Constructor.apply(obj, arguments);
// 4. 设置原型链:将新对象的原型链接到构造函数的原型
// 这一步最好在执行构造函数前完成,但放在这里也能工作
obj.__proto__ = Constructor.prototype;
// 额外:设置 constructor 属性(虽然不太必要,但更完整)
// obj.constructor = Constructor;
// 5. 返回对象:处理构造函数返回值(与上面 ES6 版本逻辑相同)
const isObject = typeof result === 'object' && result !== null;
const isFunction = typeof result === 'function';
if (isObject || isFunction) {
return result;
}
return obj;
}
4. 测试一下效果!
有了这个 ,咱们来验证一下它是不是真的拥有 的魔力:
JavaScript
function Person(name, age) {
this.name = name;
this.age = age;
// 故意返回一个非对象,例如 'hello',确保返回的是 obj
// return 'hello';
}
Person.prototype.species = '人类';
Person.prototype.sayHi = function () {
console.log('你好,我是' + this.name);
};
// 使用原生的 new
let p = new Person('张三', 18);
console.log(p.name, p.species); // 输出:张三 人类
// 使用我们手写的 objectFactory
let zzp = objectFactory(Person, '郑zp', 18);
console.log(zzp.name, zzp.species); // 预期输出:郑zp 人类
zzp.sayHi(); // 预期输出:你好,我是郑zp
完美!我们通过 成功创建了一个新对象 ,它不仅拥有 和 属性(构造函数执行的结果),还能访问到 上的 和 (原型链链接的结果)!这充分证明了我们的实现是正确的!
三、扩展阅读:如何把类数组变真数组?
既然提到了 这个类数组,咱们就顺便解决一下它的痛点:不能用数组方法!
将类数组变成真数组,在 中有几种常见且优雅的方法:
1. :扩展运算符()⭐
这是最简洁现代的方式!
JavaScript
function add(...args) { // 直接使用 rest 运算符
return args.reduce((prev, cur) => prev + cur, 0);
}
function legacyAdd() {
// 将 arguments 转换为真正的数组
const args = [...arguments];
console.log(Object.prototype.toString.call(args)); // [object Array]
return args.reduce((prev, cur) => prev + cur, 0);
}
console.log(legacyAdd(1, 2, 3)); // 输出:6
2. : 🐂
这是 时代最经典的“借用”方法:
JavaScript
function convertToArray() {
// 借用 Array.prototype 上的 slice 方法
// slice() 不传参数时会返回原数组的一个浅拷贝
const args = Array.prototype.slice.call(arguments);
// 或者更短一点:[].slice.call(arguments)
console.log(args.join('-')); // 成功使用数组方法
return args;
}
convertToArray('A', 'B', 'C');
方法之所以能将类数组转换为数组,是因为它只依赖于 属性和索引访问这两个类数组具备的特性。
总结陈词:成为 高手的第一步 🏁
恭喜你!你不仅学会了如何使用 ,更重要的是,你理解并亲手实现了它的底层逻辑!
掌握 new 的原理,本质上就是掌握 JavaScript 的原型式面向对象的核心:原型链。 当你清楚地知道 __proto__ 是如何被设置的,this 是如何被绑定的,以及构造函数的返回值是如何被处理的,你就真正拥有了 高手的思维。