深入解析 JavaScript 对象创建:Class、构造函数与工厂函数的对比

作者:半夏时间:2026-10-22 11:05:27浏览:111

本文详细对比了 JavaScript 中三种创建对象的方式:ES6 Class、构造函数和工厂函数。分析指出,虽然 Class 和构造函数提供了语法糖和可读性,但存在强制使用 new、违反开闭原则以及 instanceof 检查脆弱等问题。相比之下,工厂函数提供了更高的灵活性、更简单的重构路径以及更可控的 this 指向,推荐在需要高内聚低耦合的场景下优先使用。

JavaScript 对象创建的三种主要方式

进入 ES6 时代后,JavaScript 创建对象的方式更加丰富,开发者可以根据具体场景选择合适的手段。目前主要存在三种构建对象的方法:class 关键字、构造函数以及工厂函数。尽管它们的核心目的都是创建对象,但在实现机制和适用场景上存在显著差异,日常开发中需根据实际需求进行权衡选择。

接下来我们将具体展示这三种方法的具体实现形式。


// class 关键字,ES6新特性
class ClassCar {
 drive () {
  console.log('Vroom!');
 }
}
const car1 = new ClassCar();
console.log(car1.drive());
// 构造函数
function ConstructorCar () {}
ConstructorCar.prototype.drive = function () {
 console.log('Vroom!');
};
const car2 = new ConstructorCar();
console.log(car2.drive());
// 工厂函数
const proto = {
 drive () {
  console.log('Vroom!');
 }
};
function factoryCar () {
 return Object.create(proto);
}
const car3 = factoryCar();
console.log(car3.drive());

这三种方法均基于原型机制构建,且都支持在构造过程中使用私有变量。换言之,它们具备大部分相同的特性,在许多常见场景下,这三者的功能是等价的。

在 JavaScript 中,任何函数调用都会返回一个新对象。当该函数并非用于作为构造函数或类时,它通常被称为工厂函数。

ES6 中的 class 本质上是构造函数的语法糖(至少在目前的实现中如此),因此下文讨论的许多结论同样适用于构造函数和 ES6 类。


class Foo {}
console.log(typeof Foo); // function

构造函数与 ES6 类的优缺点分析

构造函数和 ES6 类的优势分析

  • 大多数技术书籍推荐开发者使用类和构造函数进行对象创建。
  • 关键字 this 会自动指向新创建的对象实例。
  • 部分开发者偏好 new 关键字带来的代码可读性。
  • 此外,两者之间可能存在一些细微的细节差异,但若在开发过程中未遇到明显问题,通常无需过度担忧。

构造函数和 ES6 类的局限性分析

1. 必须使用 new 关键字

在 ES6 规范下,无论是构造函数还是类,调用时都必须携带 new 关键字。


function Foo() {
 if (!(this instanceof Foo)) { return new Foo(); }
}

如果在 ES6 中尝试调用类函数而省略 new 关键字,引擎会抛出错误。若希望不使用 new 关键字,则必须使用工厂函数进行封装。

2. 实例化细节暴露给外部 API

所有调用逻辑都与构造器的具体实现紧密耦合。如果需要在构造过程中进行自定义调整,这将变得非常棘手。

3. 构造器未遵循开闭原则(Open/Closed Principle)

由于 new 关键字处理机制的限制,构造器违反了开闭原则:API 应当对扩展开放,对修改关闭。

我曾质疑类与工厂函数的相似性,认为将类函数升级为工厂函数不应产生太大影响,但在 JavaScript 中,这种转换确实存在显著差异。

如果你在编写构造函数或类时,后期发现需要工厂函数的灵活性,此时不能简单地修改函数定义即可解决问题。

不幸的是,作为 JavaScript 程序员,将构造器改造为工厂函数往往需要大规模的重构:


// 原来的实现:
// class Car {
//  drive () {
//   console.log('Vroom!');
//  }
// }
// const AutoMaker = { Car };
// 工厂函数改变的实现:
const AutoMaker = {
 Car (bundle) {
  return Object.create(this.bundle[bundle]);
 },
 bundle: {
  premium: {
   drive () {
    console.log('Vrooom!');
   },
   getOptions: function () {
    return ['leather', 'wood', 'pearl'];
   }
  }
 }
};
// 期望中的用法是:
const newCar = AutoMaker.Car('premium');
newCar.drive(); // 'Vrooom!'
// 但是因为他是一个库
// 许多地方依然这样用:
const oldCar = new AutoMaker.Car();
// 如此就会导致:
// TypeError: Cannot read property 'undefined' of
// undefined at new AutoMaker.Car

在上述示例中,我们从类开始,最终将其重构为根据特定原型创建对象的工厂函数。这种函数可广泛应用于接口抽象和特殊需求定制。

4. 构造器使 instanceof 检查变得不可靠

构造函数与工厂函数的主要区别在于 instanceof 操作符。许多人使用 instanceof 来确保代码类型的正确性,但实际上这种做法存在较大隐患,建议尽量避免使用。

instanceof 可能会给出误导性结果。


// instanceof 是一个原型链检查
// 不是一个类型检查
// 这意味着这个检查是取决于执行上下文的,
// 当原型被动态的重新关联,
// 你就会得到这样令人费解的情况
function foo() {}
const bar = { a: 'a'};
foo.prototype = bar;
// bar是一个foo的实例吗,显示不是
console.log(bar instanceof foo); // false
// 上面我们看到了,他的确不是一个foo实例
// baz 显然也不是一个foo的实例,对吧?
const baz = Object.create(bar);
// ...不对.
console.log(baz instanceof foo); // true. oops.

instanceof 并不像强类型语言那样进行严格的类型检查,它仅检查对象的原型链。

在某些执行上下文中,该检查会失效,例如当你修改了 Constructor.prototype 时。

此外,如果你最初编写的是构造函数或类,随后将其扩展为另一个对象(如上文改写为工厂函数的情况),instanceof 也会出现问题。

综上所述,instanceof 是区分构造函数和工厂函数的另一个重大差异点。

使用类的优势与潜在风险

使用类的优势

  • 作为一个便捷且自包含的关键字,
  • 它是 JavaScript 中实现类的唯一权威方式。
  • 对于拥有其他支持 class 语言开发经验的开发者而言,这提供了良好的使用体验。

使用类的劣势

构造器的所有缺点,加上:

使用 extends 关键字创建继承类对用户具有极大诱惑力。 类的层级继承会导致诸多知名问题,包括脆弱基类问题(Fragile Base Class,基础类会因继承而被破坏)、大猩猩香蕉问题(Gorilla Banana Problem,对象混杂复杂上下文)以及必要性重复(Duplication by Necessity,类在继承多样化时需频繁修改)等。

虽然其他两种方法也可能导致类似问题,但在使用 extend 关键字时,环境因素会引导你走上这条路径。换句话说,它引导你编写缺乏灵活性而非高复用性的代码。

工厂函数的优势与局限性

使用工厂函数的优势

工厂函数比类和构造函数更加灵活,不会引导用户走向错误的道路,也不会使其陷入深层的继承链中。你可以利用多种手段来模拟继承行为。

1. 返回任意原型的任意对象

例如,你可以通过同一实现创建不同实例:一个媒体播放器可根据不同媒体格式创建实例,使用不同的 API;或者一个事件库可针对 DOM 事件或 WebSocket 事件进行定制。

工厂函数还可以通过执行上下文来实例化对象,从而从对象池中受益,并支持更灵活的继承模型。

2. 无需担忧复杂的重构过程

你永远不会面临将工厂函数转换为构造函数的需求,因此无需进行此类重构。

3. 无需使用 new 关键字

你不需要使用 new 关键字来创建对象,从而可以完全掌控对象的创建过程。

4. 标准的 this 行为

this 指向你熟悉的上下文,你可以利用它获取父对象。例如,在 player.create() 中,this 指向 player,也可以通过 call 和 apply 绑定其他 this 值。

5. 无需担心 instanceof 的问题

6. 部分开发者偏好这种直接且不带 new 的写法的直观可读性。

工厂函数的劣势

  • 工厂函数不会自动处理原型,其原型不会自动波及原型链。
  • this 不会自动指向工厂函数内部的新对象。
  • 可能还存在一些细微的细节差异,但若在开发过程中未遇到明显问题,通常无需过度担忧。

总结与建议

结论

在我看来,类虽然是一个方便的关键字,但无法掩盖它将毫无防备的用户引入继承深坑的风险。另一个风险在于,未来若希望使用工厂函数,可能需要进行巨大的改动。

如果在大型团队协作中修改公共 API,可能会干扰到你无法直接控制的代码,因此不能忽视改装函数带来的影响。

工厂模式的一个显著优点在于,它不仅更加强大和灵活,还能鼓励整个团队使 API 更加简单、安全和轻量。

总结

以上所述是小编给大家介绍的详解 Javascript 中的 class、构造函数、工厂函数,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对本站网站的支持!

作者最新文章