深入理解 JavaScript 的 new 关键字:手写 new 实现
引言
new 关键字就像是一位默默工作的幕后英雄,每次你用它来实例化一个对象时,它都在背后精心安排着一切——从设置原型链到绑定 this。但你有没有好奇过,当执行 new 的那一刻,JavaScript 究竟是如何完成这些任务的?如果有一天 new 关键字突然消失,我们还能不能创建出功能完备的对象呢?
本文将深入探讨 new 关键字的工作流程,并通过一个自定义的工厂函数 objectFactory 来模拟 new 的行为。我们将结合具体的代码示例来解释每个步骤。
new 关键字的工作流程
当使用 new 创建一个对象时,JavaScript 会执行以下四个主要步骤:
- 创建新对象:创建一个新的空对象。
- 设置原型:新对象的属性
__proto__一开始都是指向Object.prototype,然后将新对象的内部属性__proto__设置为构造函数的prototype属性。 - 绑定
this:调用构造函数时,将其内部的this绑定到新创建的对象上。 - 返回对象:如果构造函数没有返回其他对象,则默认返回新创建的对象;如果有返回值且是对象类型,则返回该对象。
手写 new 的实现
为了更清楚地展示 new 的工作机制,我们可以编写一个名为 objectFactory 的函数来模拟这个过程。下面是一个简单的例子:
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.sayName = function () {
console.log(this.name);
};
function objectFactory() {
// Step 1: 创建一个新对象
const obj = new Object();
// Step 2: 获取构造函数(即 arguments 的第一个参数)
const Constructor = [].shift.call(arguments);
// Step 3: 将构造函数的剩余参数传递给 apply 方法,并将 this 绑定到新对象
Constructor.apply(obj, arguments);
// Step 4: 设置新对象的 __proto__ 指向构造函数的 prototype
obj.__proto__ = Constructor.prototype;
// Step 5: 返回新创建的对象
return obj;
}
// 使用自定义的 objectFactory 函数创建对象
let awei = objectFactory(Person, "awei", 18);
// 测试新创建的对象的方法
awei.sayName(); // 输出: awei
解析代码
Step 1:
我们首先创建了一个空对象 obj,这相当于 new 操作符创建的新对象的基础。
Step 2:
1. arguments
在 JavaScript 中,arguments 是一个类数组对象,它包含了传递给函数的所有参数。尽管它看起来像一个数组,但它并没有原生数组的所有方法。例如,在这里就是[Arguments] { '0': [Function: Person], '1': 'awei', '2': 18 }
2. [].shift
这里使用了空数组的原型方法 shift,即 [].shift。shift 方法会移除数组的第一个元素,并返回这个元素。如果数组为空,则返回 undefined。通常情况下,你会直接在一个数组上调用 shift。
但是在这个例子中,我们并不是在数组上调用 shift,而是在 arguments 上调用它。由于 arguments 不是真正的数组,所以我们不能直接调用 shift 方法,这就是为什么我们要使用 .call() 或 .apply() 来改变 this 的指向。
为什么可以这样做?
这是因为 JavaScript 中的对象方法(包括数组方法)通常不依赖于调用者的具体类型,而是依赖于传入的 this 上下文。只要 this 上下文符合方法期望的结构(例如,有相应的属性或方法),那么方法就可以正常工作。
3. .call(arguments)
.call() 是一个用于调用函数的方法,它可以显式地设置函数内的 this 值,并且可以传递参数给被调用的函数。在这个例子中,我们通过 [].shift.call(arguments) 来调用 shift 方法,但我们将 this 设置为 arguments,这样 shift 就会在 arguments 类数组上执行,而不是在一个空数组上。
Step 3:
接下来,我们使用 apply 方法来调用构造函数 Constructor,并将 this 指向新创建的对象 obj。同时,我们将剩余的参数传递给构造函数。
关于为什么用apply不用call可以看看:除了 call,JS 还有哪些强大的函数绑定方式?探索 JavaScript 函数绑定的多样世界 在我深入研究 setT - 掘金
Step 4:
然后,我们将新对象的 __proto__ 设置为构造函数的 prototype,从而建立了原型链。这是为了让新对象可以访问构造函数原型上的方法和属性。
Step 5:
最后,我们返回新创建的对象 obj。
结果:
实现了new,实例化对象有着Person的属性和方法。
结论
通过手写 new 的实现,不仅加深了我们对 JavaScript 对象创建过程的理解,还学习到了如何手动设置原型链、绑定 this 和传递参数。虽然实际开发中我们通常直接使用 new 关键字,但了解其背后的工作机制有助于写出更高效和更具可读性的代码。
如果本文对你有帮助,可以留下一个小小的赞,如有问题,也请指教