手写实现 JavaScript 的 new 操作符
在 JavaScript 中,new
操作符用于创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例。本文将深入探讨如何手动实现 new
操作符的功能,并解释其底层原理。
new 操作符的核心功能
当使用 new
操作符调用构造函数时,会发生以下几件事情:
- 创建一个全新的空对象
- 将这个空对象的
__proto__
指向构造函数的prototype
属性 - 将构造函数中的
this
绑定到这个新对象 - 执行构造函数中的代码
- 如果构造函数没有返回对象,则返回这个新对象
手动实现 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
操作符的底层行为可以描述为:
- 调用函数的
[[Construct]]
内部方法 - 创建一个新对象并将
this
绑定到该对象 - 设置对象的原型链
- 执行函数体
- 根据返回值决定最终返回的对象
__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 强大表现力的体现。