告别迷茫:手把手教你手写 new 运算符,搞定面试中的硬核问题

74 阅读4分钟

引言

在 JavaScript 的编程世界中,new 运算符犹如一座桥梁,它连接了面向对象编程的核心概念和原型链机制。作为创建用户定义对象实例的关键字,new 不仅简化了对象的初始化过程,还隐藏了许多底层细节,使得开发者可以专注于业务逻辑的实现。

new 是什么?

17.jpg

new 运算符允许开发者通过构造函数创建一个新的对象实例。当使用 new 调用构造函数时,JavaScript 会执行一系列预定义的操作,确保新对象被正确初始化。这些操作包括创建一个空对象、设置其原型链接、绑定 this 到新对象以及最终返回这个对象。

new 的工作流程

下面是一段使用 new 关键字的代码示例:

function Person(name, age) {
    this.name = name;
    this.age = age;
}
Person.prototype.sayName = function () {
    console.log(this.name);
}
const awei = new Person('awei', 20)

在这个例子中,我们创建了一个构造函数Person,当我们调用 new Person('awei', 20) 时,发生了以下步骤:

  1. 创建新对象:JavaScript 引擎首先创建了一个全新的空对象 {}
  2. 设置原型链:接着,该新对象的内部属性 [[Prototype]](可通过 __proto__ 访问)被设置为 Person.prototype,建立了新对象与构造函数之间的原型链连接。
  3. 绑定 this:然后,构造函数内的 this 被绑定到了新创建的对象上,使得我们可以在构造函数内部对新对象进行属性和方法的添加。
  4. 返回新对象:最后,如果构造函数没有显式返回另一个对象,则默认返回新创建的对象。

如何实现手写 new

为了更好地理解 new 的工作方式,我们可以尝试自己实现一个简易版本的 new

function Person(name, age) {
    this.name = name;
    this.age = age;
}

Person.prototype.sayName = function () {
    console.log(this.name);
}

function objectFactory() {
    //console.log(arguments, arguments.length);
    const obj = new Object();  //空对象创建
    const Constructor = [].shift.call(arguments);  //取出第一个参数
    //console.log(Constructor);
    obj.__proto__ = Constructor.prototype;
    // 使用 apply 调用构造函数,并传入正确的参数
    Constructor.apply(obj, arguments);
    //console.log(obj);
    return obj;
}

//测试
let awei = objectFactory(Person, 'awei', 20);
console.log(awei.name);
console.log(awei.age);
awei.sayName();

image.png

相关分析:

解析调试日志的作用:

  • console.log(arguments, arguments.length);:这条语句的作用是输出传递给 objectFactory 函数的所有参数,帮助我们理解传入了哪些值。这对于调试非常有用,可以清楚地看到每个参数的位置及其值。
  • console.log(Constructor);:这里打印出我们将要使用的构造函数,即 Person,确保我们正确地提取了构造函数。
  • console.log(obj);:此行用于确认新创建的对象是否已经成功设置了其 [[Prototype]] 属性,指向了正确的原型对象。

new 的步骤实现:

  • 空对象的创建const obj = new Object();:JavaScript 引擎会生成一个全新的空对象 {}。这个对象就像是一个空白画布,准备接受构造函数赋予它的属性和方法。
  • 原型链的设置obj.__proto__ = Constructor.prototype;:或者更推荐的方式是使用 Object.create(Constructor.prototype),这一步骤建立了新对象与构造函数之间的原型链连接,使得新对象可以继承构造函数原型上的所有属性和方法。
  • this 指针的绑定Constructor.apply(obj, arguments);:构造函数内的 this 被绑定到新创建的对象上。通过这种方式,我们在构造函数内部对 this 的任何操作实际上都是在修改或扩展新对象本身。

通过这三个步骤,new 运算符成功地将一个普通的构造函数调用转变为一个创建并初始化新对象的过程。

arguments 的作用:

arguments 是一个类数组对象,它包含传递给函数的所有参数。由于 arguments 并不是真正的数组,因此它不具有某些数组特有的方法如 shift()。为了解决这个问题,我们借用数组的方法并通过 .call() 将其应用于 arguments,从而实现了移除并获取第一个参数的功能——即构造函数本身。这种方法巧妙地绕过了 arguments 缺少数组方法的问题,同时保持了代码的简洁性和可读性。

类数组与数组的区别:

类数组对象拥有类似数组的特性,例如长度属性 length 和按索引访问元素的能力,但它并不具备真正的数组所具有的方法集,比如 push, pop, forEach 等等。而真正的数组不仅有这些方法,还实现了更丰富的接口,可以进行更多种类的操作。理解两者之间的区别有助于更好地处理函数参数和其他场景中的类数组结构。

结语

手写 new 就像是拆解一台精密的机器,让您亲手探索 JavaScript 对象创建的秘密。通过实现 new,您不仅能深入理解构造函数、原型链和 this 绑定的工作原理,还能像一位技艺娴熟的工匠一样,编写出更加高效和优雅的代码。

在面试中,能够清晰地解释并手写 new 的实现,就像是展示您的“编程内功”。这不仅能让面试官眼前一亮,还能证明您对 JavaScript 内部机制有着深刻的理解,帮助您在众多候选人中脱颖而出,顺利拿下 Offer!🚀

16.jpg