手写 JavaScript 的 new 操作符:从空对象到完整实例的诞生过程

手写实现 JavaScript 的 new 操作符

在 JavaScript 中,new 操作符用于创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例。本文将深入探讨如何手动实现 new 操作符的功能,并解释其底层原理。

new 操作符的核心功能

当使用 new 操作符调用构造函数时,会发生以下几件事情:

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

手动实现 new 操作符

原始版本实现

javascript

javascript 复制代码
function objectFactory() {
    var obj = {}  // 1. 创建新对象
    
    // 类数组上没有shift方法,所以借用数组的shift方法
    var Constructor = [].shift.call(arguments)  // 获取构造函数
    
    var res = Constructor.apply(obj, arguments)  // 3. 绑定this并执行构造函数
    obj.__proto__ = Constructor.prototype      // 2. 设置原型链
    
    // 处理构造函数返回值:
    // - 如果返回对象,则使用该对象
    // - 如果返回null或其他基本类型,则忽略返回新对象
    return typeof res === 'object' ? res || obj : obj
}

ES6 改进版本

javascript

javascript 复制代码
function objectFactory(Constructor, ...args) {
    var obj = {}  // 创建新对象
    
    // 使用剩余参数替代arguments处理
    var res = Constructor.apply(obj, args)  // 绑定this并执行构造函数
    obj.__proto__ = Constructor.prototype   // 设置原型链
    
    return typeof res === 'object' ? res || obj : obj
}

关键点解析

1. 创建新对象

首先创建一个空对象 {},这将作为新实例的基础。

2. 获取构造函数

在原始版本中,我们从 arguments 中提取构造函数。由于 arguments 是类数组对象,没有 shift 方法,我们通过借用数组的 shift 方法来实现:
[].shift.call(arguments)

3. 绑定 this 并执行构造函数

使用 apply 方法将构造函数的 this 绑定到新创建的对象,并传入参数执行构造函数。

4. 设置原型链

将新对象的 __proto__ 指向构造函数的 prototype 属性,这样实例就能访问构造函数原型上的方法和属性。 在 JavaScript 中,__proto__ 是一个非标准但被广泛实现的属性,用于访问和修改对象的原型。虽然它在手动实现 new 操作符时很常见,但直接使用它存在一些问题和风险。(问题见下文)

5. 处理构造函数返回值

这是实现中最关键的部分之一:

  • 如果构造函数返回一个对象(包括数组、函数等),则使用该返回值
  • 如果构造函数返回 null 或其他基本类型(如数字、字符串等),则忽略返回值,返回新创建的对象

测试用例

javascript

javascript 复制代码
function Person(name, age) {
    this.name = name
    this.age = age
    // 可以测试不同返回值情况
    // return 1           // 基本类型,会被忽略
    // return {           // 对象,会被使用
    //     name: name,
    //     age: age,
    // }
    return null         // null,会被忽略
}

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

let p = objectFactory(Person, '张三', 18)
console.log(p);
p.sayHi()
console.log(p instanceof Person)  // 应该返回true

底层原理

new 操作符的底层行为可以描述为:

  1. 调用函数的 [[Construct]] 内部方法
  2. 创建一个新对象并将 this 绑定到该对象
  3. 设置对象的原型链
  4. 执行函数体
  5. 根据返回值决定最终返回的对象

__proto__ 的主要问题

1. 非标准属性

__proto__ 最初是浏览器厂商引入的特性,后来被纳入 ECMAScript 2015 (ES6) 规范作为遗留特性,但不是语言标准的核心部分。这意味着:

  • 不是所有 JavaScript 环境都保证支持它
  • 未来的规范可能会废弃或修改它的行为
  • 在严格模式下可能受限

2. 性能问题

直接操作 __proto__ 会触发浏览器的慢路径操作,因为:

  • 它破坏了 JavaScript 引擎对对象结构的静态分析
  • 导致引擎无法进行某些优化
  • 可能触发整个原型链的重新计算

3. 安全性问题

允许动态修改原型链可能导致:

  • 原型污染攻击:恶意代码可以通过修改基础对象的原型来影响所有继承对象
  • 意外的全局影响:修改内置对象原型会影响所有相关实例

4. 可维护性问题

代码中直接使用 __proto__:

  • 降低了代码的可读性和可维护性
  • 使静态类型检查更加困难

推荐替代方案

1. 使用 Object.create()

创建对象并设置其原型的标准方法:

javascript

javascript 复制代码
function objectFactory(Constructor, ...args) {
    // 使用 Object.create 替代 new Object 和 __proto__ 赋值
    var obj = Object.create(Constructor.prototype)
    var res = Constructor.apply(obj, args)
    return typeof res === 'object' ? res || obj : obj
}

2. 使用 Object.setPrototypeOf()

修改现有对象原型的标准方法:

javascript

javascript 复制代码
function objectFactory(Constructor, ...args) {
    var obj = {}
    Object.setPrototypeOf(obj, Constructor.prototype)
    var res = Constructor.apply(obj, args)
    return typeof res === 'object' ? res || obj : obj
}

3. 使用 ES6 的 Reflect API

更现代的原型操作方法:

javascript

javascript 复制代码
function objectFactory(Constructor, ...args) {
    var obj = {}
    Reflect.setPrototypeOf(obj, Constructor.prototype)
    var res = Constructor.apply(obj, args)
    return typeof res === 'object' ? res || obj : obj
}

总结

手动实现 new 操作符不仅帮助我们深入理解 JavaScript 的对象创建机制,还能让我们更好地掌握原型继承和 this 绑定的概念。关键点在于正确处理原型链的建立和构造函数返回值的判断。

这种实现方式展示了 JavaScript 的灵活性,我们可以通过普通函数调用来模拟语言原生的操作符行为,这也是 JavaScript 强大表现力的体现。

相关推荐
IT_陈寒4 小时前
Java中equals方法比了个寂寞?原来这才是正确的重写姿势
前端·人工智能·后端
默_笙4 小时前
🚓 分诊台与拆题术:让 RAG 学会判断和规划
前端·javascript
卷福同学4 小时前
第一次当面试官有感
后端·面试
CopyCode4 小时前
用 AI 迁项目有多爽?我把 Webpack 迁 Vite 的全过程记下来了
前端·架构
去伪存真4 小时前
Electron 自动化发布指南:GitHub Actions 跨平台打包全纪录
前端·electron
计算机魔术师4 小时前
OpenAI智能体失控闯进美国政府网站,53张用户图片外泄背后
前端
颜进强4 小时前
14 · NestJS ExecutionContext 执行上下文:守卫、拦截器、过滤器拿到的"同一个 context",为什么能力不一样?
前端·后端·ai编程
程序员Flycan4 小时前
🚀 跨域终结者:前端代理服务器(Proxy)原理解析与配置总结
前端
怕浪猫4 小时前
顶级模型一句话,AI 写出了能玩的 QQ飞车
前端·面试·github
huakoh4 小时前
MCP 报错分不清?先看响应里是 result 还是 error
前端