手撕JavaScript中的new操作符:从原理到实现

154 阅读4分钟

前言

在JavaScript中,new操作符是我们创建对象实例的基本方式之一。但你是否想过new背后到底做了什么?本文将带你深入剖析new的工作原理,并手写实现一个new的功能函数。

new操作符的核心原理

当我们使用new Constructor()时,JavaScript引擎会执行以下步骤:

  1. 创建一个空对象
  2. 将这个空对象的__proto__指向构造函数的prototype属性
  3. 将构造函数的this绑定到这个新对象
  4. 执行构造函数内部的代码
  5. 如果构造函数没有返回对象,则返回这个新对象;如果返回了对象,则使用该返回值

原始版本实现

我们先来看原始版本的objectFactory实现:

function objectFactory() {
    var obj = {};
    // 类数组上没有shift方法,所以需要借用数组的shift方法
    var Constructor = [].shift.call(arguments); // 获取构造函数
    obj.__proto__ = Constructor.prototype; // 设置原型链
    var ret = Constructor.apply(obj, arguments) // 调用构造函数
    // 处理返回值:如果返回的是对象则使用返回值,否则返回obj
    return typeof ret === 'object' ? ret || obj : obj;
}

前置文档分析

  1. 如果你不了解argumentsJavaScript 中的 arguments、柯里化和展开运算符详解
  2. 如果你不了解__proto__prototypeJavaScript 原型与原型链:深入理解 proto 和 prototype 的由来与关系
  3. 如果你不了解callapply🛸🛸谁在调用我?深入 JavaScript中 this的指向之谜
  4. 如果你还有其他不懂得请自行搜索吧🚀🚀我也没有办法

代码解析

  1. 创建空对象var obj = {}创建一个全新的空对象。
  2. 获取构造函数[].shift.call(arguments)是一个技巧,它借用数组的shift方法从arguments类数组对象中取出第一个参数(即构造函数),并且会将第一个参数从arguments中删除。
  3. 设置原型链obj.__proto__ = Constructor.prototype将新对象的原型指向构造函数的原型,这样实例就能访问构造函数原型上的方法和属性。
  4. 调用构造函数Constructor.apply(obj, arguments)使用apply方法调用构造函数,将this绑定到新创建的对象上,并传入剩余参数。
  5. 处理返回值:通过typeof ret === 'object'判断构造函数是否返回对象,如果是则返回该对象(ret || obj处理了retnull的情况),否则返回新创建的对象。

ES6优化版本

原始版本使用了arguments对象和[].shift.call这样的技巧,ES6版本可以更简洁:

function objectFactory(Constructor, ...args) {
    var obj = {};
    obj.__proto__ = Constructor.prototype;
    var ret = Constructor.apply(obj, args)
    return typeof ret === 'object' ? ret || obj : obj;
}

优化点分析

  1. 参数处理更清晰:使用剩余参数...args直接获取构造函数后的所有参数,不再需要arguments对象和shift技巧。
  2. 代码更简洁:减少了不必要的变量声明和复杂的参数处理。
  3. 可读性更好:参数结构一目了然,函数签名更清晰。

对比分析

特性原始版本ES6优化版本
参数处理使用argumentsshift技巧使用剩余参数...args
代码简洁性较复杂更简洁
可读性一般更好
兼容性更好需要ES6支持
性能稍差(需要调用shift稍好

使用示例

function Person(name, age) {
    this.name = name;
    this.age = age;
    // 可以尝试注释/取消注释下面的return语句观察不同
    // return 1; // 基本类型会被忽略
    //return {  // 对象类型会替代新创建的对象
    //    name: name,
    //    age: age,
    //    label: '哈哈'
    //}
}

Person.prototype.sayHi = function() {
    console.log(`你好,我是${this.name}`)
}

// 使用new操作符
let p1 = new Person('张三', 18)
console.log(p1)

// 使用我们的objectFactory
let p = objectFactory(Person, '张三', 18)
console.log(p)
p.sayHi()
console.log(p instanceof Person)

代码运行结果如下:

image.png

关键点总结

  1. 构造函数返回值处理

    • 如果返回基本类型(如numberstring等),会被忽略
    • 如果返回对象类型,则会替代新创建的对象
  2. 原型链设置:必须正确设置__proto__指向构造函数的prototype,这是实现继承的关键。

  3. this绑定:通过apply将构造函数的this绑定到新对象。

  4. instanceof检查:由于正确设置了原型链,我们的实现也能通过instanceof检查。

实际应用场景

理解new的内部机制对于以下场景很有帮助:

  1. 框架开发:许多框架需要自己控制对象创建过程
  2. 高级编程模式:如对象池、特定类型的对象创建控制
  3. 面试准备:这是JavaScript中常见的面试题
  4. 理解原型继承:深入理解JavaScript的原型继承机制

结语

通过手写new的实现,我们不仅更深入理解了JavaScript的对象创建机制,也掌握了如何利用原型链实现继承。ES6的语法让我们的代码更加简洁明了,但理解底层原理仍然是成为高级JavaScript开发者的必经之路。

希望这篇博客能帮助你彻底理解new操作符的工作原理!