模拟 JavaScript 的 new:创建自己的对象工厂

392 阅读4分钟

深入理解 JavaScript 的 new 关键字:手写 new 实现

引言

new 关键字就像是一位默默工作的幕后英雄,每次你用它来实例化一个对象时,它都在背后精心安排着一切——从设置原型链到绑定 this。但你有没有好奇过,当执行 new 的那一刻,JavaScript 究竟是如何完成这些任务的?如果有一天 new 关键字突然消失,我们还能不能创建出功能完备的对象呢?

本文将深入探讨 new 关键字的工作流程,并通过一个自定义的工厂函数 objectFactory 来模拟 new 的行为。我们将结合具体的代码示例来解释每个步骤。

new 关键字的工作流程

当使用 new 创建一个对象时,JavaScript 会执行以下四个主要步骤:

  1. 创建新对象:创建一个新的空对象。
  2. 设置原型:新对象的属性 __proto__一开始都是指向Object.prototype,然后将新对象的内部属性 __proto__ 设置为构造函数的 prototype 属性。
  3. 绑定 this:调用构造函数时,将其内部的 this 绑定到新创建的对象上。
  4. 返回对象:如果构造函数没有返回其他对象,则默认返回新创建的对象;如果有返回值且是对象类型,则返回该对象。

手写 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,即 [].shiftshift 方法会移除数组的第一个元素,并返回这个元素。如果数组为空,则返回 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 关键字,但了解其背后的工作机制有助于写出更高效和更具可读性的代码。

如果本文对你有帮助,可以留下一个小小的赞,如有问题,也请指教