前言
在JavaScript中,new操作符是面向对象编程的核心概念之一。它负责创建构造函数的实例对象,建立原型链关系,并执行构造函数的初始化逻辑。今天我们将深入探讨如何手写一个new操作符,并在此过程中理解JavaScript的方法借用机制。
一、new操作符的工作原理
在手写一个属于我们的new之前,我们需要理解原生new操作符的执行步骤:
- 创建空对象:创建一个新的空对象作为实例
- 绑定原型:将新对象的
__proto__指向构造函数的prototype - 执行构造函数:以新对象为
this上下文执行构造函数 - 返回对象:根据构造函数的返回值决定最终返回的对象
二、手写new操作符实现
让我们来看看完整的实现代码:
/**
* 手写一个new函数,用于创建构造函数的实例对象
* @param {Function} constructor 用于创建实例的构造函数
* @param {...any} args 传递给构造函数的参数列表
* @returns {Object} 返回一个构造函数的实例对象,该对象继承自constructor的原型
*/
function myNew(constructor, ...args) {
// 1. 校验:第一个参数必须是可构造的函数(排除箭头函数等无prototype的函数)
if (typeof constructor !== 'function' || !constructor.prototype) {
throw new TypeError(`${constructor} is not a constructor`);
}
// 2、创建一个新的空对象(实例)
const instance = {};
// 3、绑定原型链:令空对象的__proto__属性指向构造函数的prototype
Object.setPrototypeOf(instance, constructor.prototype);
// 注意!!!虽然这里等价于instance.__proto__=constructor.prototype,但是不推荐直接操作__proto__属性,因为这是浏览器实现的私有属性,其他环境可能不支持,比如nodejs环境
// 4、执行构造函数,this绑定到实例,使得实例可以使用构造函数的属性和方法(方法借用机制)
const result = constructor.apply(instance, args);
// 5. 处理返回值:若返回对象则用该对象,否则返回实例
return result instanceof Object ? result : instance;
}
三、代码详解
1. 参数校验
if (typeof constructor !== 'function' || !constructor.prototype) {
throw new TypeError(`${constructor} is not a constructor`);
}
这里进行了严格的构造函数校验:
- 基础类型检查:
typeof constructor !== 'function'确保传入的是函数 - 构造函数特性检查:
!constructor.prototype排除了不能作为构造函数的函数类型,即为没有prototype的函数。
2. 创建实例对象
const instance = {};
创建一个空对象,这个对象将成为构造函数的实例。
3. 建立原型链关系
Object.setPrototypeOf(instance, constructor.prototype);
这一步是关键,它建立了实例对象与构造函数原型之间的继承关系。我们使用Object.setPrototypeOf()而不是直接操作__proto__属性,因为:
__proto__是浏览器实现的私有属性- 在某些环境(如Node.js早期版本)中可能不支持
Object.setPrototypeOf()是标准的ES6方法,更加规范
4. 执行构造函数(方法借用机制)
const result = constructor.apply(instance, args);
这里体现了JavaScript方法借用机制的精髓。我们使用apply方法将构造函数"借用"给新创建的实例对象:
constructor:被借用的构造函数instance:借用方(新创建的实例对象)args:传递给构造函数的参数数组
通过apply,我们改变了构造函数执行时的this指向,使其指向新创建的实例对象。
补充:方法借用机制
在《少写重复代码的精髓:JS方法借用》这篇文章中,我介绍过了 JS 的方法借用机制,大家可以补补课。
5. 返回值处理
return result instanceof Object ? result : instance;
这一步处理构造函数的返回值:
- 如果构造函数返回一个对象,则使用该对象作为最终结果
- 否则,返回我们创建的实例对象
四、测试我们的myNew函数
让我们创建一些测试用例来验证我们的实现:
// 测试构造函数
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.sayHello = function() {
console.log(`Hello, I'm ${this.name}, ${this.age} years old.`);
};
// 使用我们的myNew函数
const person1 = myNew(Person, 'Alice', 25);
console.log(person1.name); // "Alice"
console.log(person1.age); // 25
person1.sayHello(); // "Hello, I'm Alice, 25 years old."
// 验证原型链
console.log(person1 instanceof Person); // true
console.log(person1.__proto__ === Person.prototype); // true
// 测试构造函数返回对象的情况
function SpecialConstructor() {
this.prop = 'instance property';
return { customProp: 'custom object' };
}
const special = myNew(SpecialConstructor);
console.log(special.customProp); // "custom object"
console.log(special.prop); // undefined(因为返回了自定义对象)
与原生new的对比
让我们对比一下我们的实现与原生new操作符:
// 原生new
const nativePerson = new Person('Bob', 30);
// 我们的myNew
const customPerson = myNew(Person, 'Bob', 30);
// 两者行为完全一致
console.log(nativePerson instanceof Person); // true
console.log(customPerson instanceof Person); // true
console.log(nativePerson.constructor === Person); // true
console.log(customPerson.constructor === Person); // true
五、手写new的优化
1. 使用Object.create优化原型设置
function myNewOptimized(constructor, ...args) {
if (typeof constructor !== 'function') {
throw new TypeError('Constructor must be a function');
}
// 使用Object.create直接创建具有指定原型的对象
const instance = Object.create(constructor.prototype);
const result = constructor.apply(instance, args);
return result instanceof Object ? result : instance;
}
2. 更严格的返回值检查
function myNewAdvanced(constructor, ...args) {
if (typeof constructor !== 'function') {
throw new TypeError('Constructor must be a function');
}
const instance = Object.create(constructor.prototype);
const result = constructor.apply(instance, args);
// 更严格的对象检查:包括null检查和函数类型
return (typeof result === 'object' && result !== null) || typeof result === 'function'
? result
: instance;
}
- 改进点说明:
- 原版本使用
result instanceof Object,但这不能正确处理null(因为null instanceof Object返回false,但typeof null === 'object') - 新版本明确检查
result !== null,避免了这个JavaScript的历史遗留问题 - 同时支持构造函数返回函数的情况(虽然很少见,但符合规范)
- 原版本使用
结语
通过手写new操作符,我们深入理解了JavaScript的核心机制:对象创建、原型链绑定、执行上下文切换和方法借用。这个过程让我们认识到,理解底层原理比记忆API更重要。当我们掌握了apply的本质,就能理解为什么它既能用于构造函数调用,也能用于数组方法借用。当我们理解了原型链机制,就能更好地处理继承相关的问题。
手写代码的价值在于将抽象概念具象化。原型链不再是文档中的描述,而是Object.setPrototypeOf的具体操作;方法借用不再是理论知识,而是constructor.apply(instance, args)的实际应用。
这种 "知其然,知其所以然" 的学习方式,将帮助我们在面对更复杂的技术挑战时游刃有余。JS手写系列的第一弹就这么结束了,但是我们还有更多的技术重点等我们去从源码入手理解,完全吃下。
如果这篇文章有帮助到你,不胜荣幸;如果文章有错误或者缺漏,请在评论区指出,大家一起进步,谢谢🙏。