手写 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 强大表现力的体现。

相关推荐
知识分享小能手2 小时前
Vue3 学习教程,从入门到精通,Axios 在 Vue 3 中的使用指南(37)
前端·javascript·vue.js·学习·typescript·vue·vue3
程序员码歌4 小时前
【零代码AI编程实战】AI灯塔导航-总结篇
android·前端·后端
用户21411832636025 小时前
免费玩转 AI 编程!Claude Code Router + Qwen3-Code 实战教程
前端
小小愿望7 小时前
前端无法获取响应头(如 Content-Disposition)的原因与解决方案
前端·后端
小小愿望7 小时前
项目启功需要添加SKIP_PREFLIGHT_CHECK=true该怎么办?
前端
烛阴7 小时前
精简之道:TypeScript 参数属性 (Parameter Properties) 详解
前端·javascript·typescript
海上彼尚7 小时前
使用 npm-run-all2 简化你的 npm 脚本工作流
前端·npm·node.js
开发者小天8 小时前
为什么 /deep/ 现在不推荐使用?
前端·javascript·node.js
恋喵大鲤鱼8 小时前
Golang 后台技术面试套题 1
面试·golang
why技术8 小时前
也是震惊到我了!家里有密码锁的注意了,这真不是 BUG,是 feature。
后端·面试