前言
在JavaScript中,new操作符是我们创建对象实例的基本方式之一。但你是否想过new背后到底做了什么?本文将带你深入剖析new的工作原理,并手写实现一个new的功能函数。
new操作符的核心原理
当我们使用new Constructor()时,JavaScript引擎会执行以下步骤:
- 创建一个空对象
- 将这个空对象的
__proto__指向构造函数的prototype属性 - 将构造函数的
this绑定到这个新对象 - 执行构造函数内部的代码
- 如果构造函数没有返回对象,则返回这个新对象;如果返回了对象,则使用该返回值
原始版本实现
我们先来看原始版本的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;
}
前置文档分析
- 如果你不了解
arguments:JavaScript 中的 arguments、柯里化和展开运算符详解 - 如果你不了解
__proto__和prototype:JavaScript 原型与原型链:深入理解 proto 和 prototype 的由来与关系 - 如果你不了解
call和apply:🛸🛸谁在调用我?深入 JavaScript中 this的指向之谜 - 如果你还有其他不懂得请自行搜索吧🚀🚀我也没有办法
代码解析
- 创建空对象:
var obj = {}创建一个全新的空对象。 - 获取构造函数:
[].shift.call(arguments)是一个技巧,它借用数组的shift方法从arguments类数组对象中取出第一个参数(即构造函数),并且会将第一个参数从arguments中删除。 - 设置原型链:
obj.__proto__ = Constructor.prototype将新对象的原型指向构造函数的原型,这样实例就能访问构造函数原型上的方法和属性。 - 调用构造函数:
Constructor.apply(obj, arguments)使用apply方法调用构造函数,将this绑定到新创建的对象上,并传入剩余参数。 - 处理返回值:通过
typeof ret === 'object'判断构造函数是否返回对象,如果是则返回该对象(ret || obj处理了ret为null的情况),否则返回新创建的对象。
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;
}
优化点分析
- 参数处理更清晰:使用剩余参数
...args直接获取构造函数后的所有参数,不再需要arguments对象和shift技巧。 - 代码更简洁:减少了不必要的变量声明和复杂的参数处理。
- 可读性更好:参数结构一目了然,函数签名更清晰。
对比分析
| 特性 | 原始版本 | ES6优化版本 |
|---|---|---|
| 参数处理 | 使用arguments和shift技巧 | 使用剩余参数...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)
代码运行结果如下:
关键点总结
-
构造函数返回值处理:
- 如果返回基本类型(如
number,string等),会被忽略 - 如果返回对象类型,则会替代新创建的对象
- 如果返回基本类型(如
-
原型链设置:必须正确设置
__proto__指向构造函数的prototype,这是实现继承的关键。 -
this绑定:通过
apply将构造函数的this绑定到新对象。 -
instanceof检查:由于正确设置了原型链,我们的实现也能通过
instanceof检查。
实际应用场景
理解new的内部机制对于以下场景很有帮助:
- 框架开发:许多框架需要自己控制对象创建过程
- 高级编程模式:如对象池、特定类型的对象创建控制
- 面试准备:这是JavaScript中常见的面试题
- 理解原型继承:深入理解JavaScript的原型继承机制
结语
通过手写new的实现,我们不仅更深入理解了JavaScript的对象创建机制,也掌握了如何利用原型链实现继承。ES6的语法让我们的代码更加简洁明了,但理解底层原理仍然是成为高级JavaScript开发者的必经之路。
希望这篇博客能帮助你彻底理解new操作符的工作原理!