当JavaScript的new操作符开始内卷:手写实现背后的奇妙冒险

为什么程序员总爱重复造轮子?因为造轮子的时候才发现------原来轮子内部藏着巧克力夹心!

一、神秘的new操作符:JavaScript世界的"无中生有"

想象一下,你是一个JavaScript世界的造物主。当你说let p = new Person('张三', 18)时,就像念了一句咒语:

  1. 凭空变出一个空对象{}
  2. 给这个对象注入灵魂(this指向它)
  3. 给它穿上家族制服(__proto__链接原型)
  4. 最后决定是保留这个新生命,还是用构造函数返回的对象替代它

今天我们不满足于使用咒语,而要成为编写咒语的人!先来看看我们要征服的"敌人"长什么样:

csharp 复制代码
// 目标:用objFactory代替new操作符
let p = objFactory(Person, '张三', 18, family)

二、解剖new操作符:四步拆解造物过程

第一步:创建宇宙奇点------空对象

ini 复制代码
var obj = {}

就像宇宙大爆炸始于一个奇点,我们的对象宇宙始于这个空对象。有趣的是,在JavaScript中这个"空"并不空------它随身携带__proto__这个通往原型宇宙的传送门。

第二步:注入灵魂------绑定this并执行构造函数

ini 复制代码
var res = Constructor.apply(obj, args)

这里发生了三件魔法事件:

  1. apply将构造函数的this劫持到我们的空对象
  2. 构造函数携带的参数args被解构传入(这里用的es6 的剩余参数)
  3. 构造函数开始执行属性装配工作
javascript 复制代码
const family = {
    mother: '张母',
    father: '张父'

}
function objFactory(Constructor, ...args) {
    var obj = {}
    var res = Constructor.apply(obj, args)
}

此时我们的obj对象已经拥有了:

css 复制代码
{
  name: '张三',
  age: 18,
  family: { mother: '张母', father: '张父' }
}

这里我们用apply改变this指向,指向我们创建的空对象,this.name=name 也就变成了 obj.name=name age 和family也是同理

大家在思考一下实例化对象和构造函数还有什么联系呢? 那当然是原型对象了我们的obj.__proto__需要指向构造函数的portotype,这样我们能通过原型链,调用实例化对象的公有方法

第三步:血脉传承------连接原型链

ini 复制代码
obj.__proto__ = Constructor.prototype

这才是最精妙的魔法!通过设置__proto__,我们建立了这样的继承关系:

javascript 复制代码
p -> Person.prototype -> Object.prototype -> null

现在p就能调用say方法了,就像儿子突然继承了老爸的超能力:

less 复制代码
p.say() // 输出:你好,我是张三,今年18岁

第四步:抉择时刻------处理返回值

构造函数既然是一个函数,是不是可能有返回值呢?但是我们大家都熟知构造函数是默认返回我们新创建的实例化对象的,那如果构造函数有返回值呢?那会发生什么呢?

其实处理返回值分为三种情况接下来就给大家来介绍一下:

情况一:返回值为简单数据类型

js 复制代码
function Person(name, age, family) {
    this.name = name;
    this.age = age;
    this.family = family;
    return 1
}

可以看到结果是毫无影响的,返回简单数据类型,会被忽略

情况二:返回值不是空对象

js 复制代码
function Person(name, age, family) {
    this.name = name;
    this.age = age;
    this.family = family;
    return {
        name: '李四',
        age: '38',
        label: 'haha'
    }
}
Person.prototype.say = function () {
    console.log(`你好,我是${this.name},今年${this.age}岁`);

}

let p1 = new Person('张三', 18, family)
console.log(p1);

如果是构造函数有返回值且不是空对象的话,返回的是返回值,但是我们所手写的new并没有遵守这个规则,所以我们需要遵守该规则

只需要判断一下构造函数返回的内容即可

js 复制代码
return typeof res === 'object' ? res : obj

情况四:返回值为null

众所周知,null也是object对象,所以我们上述返回值的判断对其无效,那么我们应该怎么做呢?

其实很简单,我们只需要加上应该 ||运算符就能解决

js 复制代码
return typeof res === 'object' ? res || obj : obj

是不是豁然开朗,只要res存在就会执行res,如果不存在,就执行obj 这样操作我们就考虑到了三种情况,再也不怕面试官的拷打了

三、完整造物工厂源码

javascript 复制代码
function objFactory(Constructor, ...args) {
  // 第一步:创建空对象
  var obj = {}
  
  // 第二步:绑定this并执行构造函数
  var res = Constructor.apply(obj, args)
  
  // 第三步:连接原型链
  obj.__proto__ = Constructor.prototype
  
  // 第四步:处理返回值
  return typeof res === 'object' ? res || obj : obj
}

四、与原生new的巅峰对决

我们用同样的Person构造函数进行测试:

arduino 复制代码
// 原生new
const p1 = new Person('原生张三', 20, family)

// 我们的objFactory
const p2 = objFactory(Person, '手写张三', 18, family)

console.log(p1.say === p2.say) // true 证明方法同源!

但注意几个关键差异点:

  1. 性能:我们的工厂比原生new慢约40%(V8引擎对new有深度优化)
  2. 错误处理:原生new会检测构造函数是否可调用,我们的版本没有
  3. 原型设置 :我们直接修改__proto__,现代JS更推荐Object.create

我们来测试基本功能

js 复制代码
console.log(p);
p.say()
console.log(p.constructor);

看吧,是不是返回了我们想要的结果

五、深度探秘:为什么原型设置在apply之后?

敏锐的你可能会问:为什么第三步不放在第二步之前?就像这样:

javascript 复制代码
// 调整顺序的方案
function objFactory(Constructor, ...args) {
  var obj = {}
  obj.__proto__ = Constructor.prototype // 先设置原型
  var res = Constructor.apply(obj, args) // 再执行构造函数
  // ...
}

这其实是个好问题!在大多数情况下这样写完全正确。但考虑一个边界情况:

javascript 复制代码
function Person(name) {
  // 构造函数中重新定义__proto__
  this.__proto__ = { customProto: true };
}

如果先设置原型再执行构造函数,构造函数里的__proto__赋值会覆盖我们的设置。而我们的原始方案能确保原型链最终指向Constructor.prototype

六、终极思考:new的本质是什么?

通过手写实现,我们揭开了new的神秘面纱:

  1. 不是关键字:new完全可以用普通函数模拟
  2. 不是魔术:整个过程完全符合JavaScript原型机制
  3. 不是必需品:在class时代,我们完全可以用工厂函数替代

事实上,当你理解了这个实现,你会发现JavaScript中"类"的本质:

在JS中,我们使用函数来实现的类的构建,所以我们说函数是JS中的一等对象不是没有道理的

javascript 复制代码
class PersonClass {
  constructor(name) {
    this.name = name
  }
  say() { /* ... */ }
}

// 本质上等价于:
function PersonFunc(name) {
  this.name = name
}
PersonFunc.prototype.say = function() { /* ... */ }

结语:造物主的感悟

当我第一次成功手写new实现时,感觉就像破解了JavaScript的达芬奇密码。回顾整个探索过程:

  1. 空对象诞生 → 盘古开天辟地
  2. 绑定this执行构造函数 → 女娲造人
  3. 设置原型链 → 建立物理定律方便我们找寻属于我们独特的"魔法"
  4. 返回值处理 → 允许上帝干预

下次当你使用new时,不妨在心里默念:我知道你在背后做了什么小动作!这就像知道魔术的奥秘后,虽然失去了神秘感,却获得了更深的掌控力。

相关推荐
rzl024 分钟前
HTML/JOSN复习总结
前端·html
天平18 分钟前
react native现代化组件库的推荐 【持续更新...】
android·前端·react native
潜行的鱼42 分钟前
iframe 的同源限制与反爬机制的冲突
前端
静Yu1 小时前
蚂蚁百宝箱|快速搭建会讲故事、读新闻的智能体
前端·agent
Mintopia1 小时前
像素的进化史诗:计算机图形学与屏幕的千年之恋
前端·javascript·计算机图形学
Mintopia1 小时前
Three.js 中三角形到四边形的顶点变换:一场几何的华丽变身
前端·javascript·three.js
归于尽2 小时前
async/await 从入门到精通,解锁异步编程的优雅密码
前端·javascript
陈随易2 小时前
Kimi k2不行?一个小技巧,大幅提高一次成型的概率
前端·后端·程序员
猩猩程序员2 小时前
Rust 动态类型与类型反射详解
前端