news 2026/9/3 10:13:57

JavaScript对象创建的五种核心方式:从工厂模式到ES6类语法

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JavaScript对象创建的五种核心方式:从工厂模式到ES6类语法

1. 从“new Object()”说起:为什么我们需要五种创建方式?

在JavaScript的世界里,对象是构建一切的基石。无论是前端页面的DOM操作,还是后端的Node.js服务,都离不开对象的创建与操作。很多刚入门的开发者,可能只知道一两种创建对象的方式,比如最直观的new Object()或者字面量{}。但当你深入项目,面对不同的场景——比如需要复用模板、创建大量相似实例、或者实现私有属性时——你会发现,单一的方式往往力不从心,甚至可能引入性能或设计上的问题。

这就是为什么理解JS中对象创建的五种核心方式至关重要。这五种方式并非简单的语法差异,它们背后对应着不同的设计模式、内存管理策略和应用场景。掌握它们,意味着你能在合适的场景选择最合适的工具,写出更高效、更健壮、更易维护的代码。今天,我们就来彻底拆解这五种方式:工厂模式、构造函数模式、原型模式、组合使用构造函数和原型模式,以及ES6的类语法。我会结合我多年踩坑的经验,告诉你每种方式的“为什么”和“怎么选”,而不仅仅是“是什么”。

2. 工厂模式:快速批量的“简易车间”

当你需要创建一系列结构相似但数据不同的对象时,第一个跳入脑海的可能是写一个函数来封装创建过程。这就是工厂模式的核心思想——像一个简易车间,接收原料(参数),返回产品(对象)。

2.1 基本实现与典型代码

工厂模式不涉及new操作符和特定的构造函数。它就是一个普通的函数,在函数内部手动创建一个新对象,为其添加属性和方法,最后返回这个对象。

function createPerson(name, age) { // 1. 手动创建一个新对象 const obj = new Object(); // 2. 为对象添加属性 obj.name = name; obj.age = age; // 3. 为对象添加方法 obj.sayName = function() { console.log(this.name); }; // 4. 返回这个对象 return obj; } // 使用工厂函数 const person1 = createPerson('张三', 25); const person2 = createPerson('李四', 30); person1.sayName(); // 输出:张三

2.2 核心优势与适用场景

工厂模式最大的优点是简单直观,隔离了创建细节。调用者无需关心对象内部是如何构建的,只需传入参数即可获得一个完整的对象。这在创建一些配置对象、数据模型或者需要一定初始化逻辑的简单对象时非常有用。

例如,在前端项目中,我们经常需要根据API返回的数据创建视图模型:

function createUserViewModel(apiData) { return { id: apiData.userId, displayName: `${apiData.firstName} ${apiData.lastName}`, avatarUrl: apiData.profileImage || '/default-avatar.png', isActive: apiData.lastLoginTime > Date.now() - 7 * 24 * 60 * 60 * 1000 }; }

2.3 无法回避的“身份”困境

然而,工厂模式有一个致命的缺陷:所有创建出来的对象都是“孤儿”,它们之间没有内在的联系。

console.log(person1 instanceof createPerson); // false console.log(person1.constructor); // [Function: Object]

person1并不是createPerson的实例,它的构造函数是顶层的Object。这意味着:

  1. 无法进行类型识别:你无法用instanceof操作符来判断一个对象是否由某个特定的工厂函数创建。这在需要做类型检查或依赖注入的场景下是个大问题。
  2. 方法重复创建,内存浪费:注意看上面的代码,每个对象都有自己的sayName方法。创建100个对象,就会在内存中有100个功能完全相同的函数副本,这无疑是巨大的浪费。

注意:虽然现代JS引擎有优化,但对于复杂的对象方法,这种浪费是实实在在的。我曾在一个需要渲染大量列表项的项目中,初期使用了类似工厂模式的方法,导致页面内存占用飙升,滚动卡顿,排查了很久才发现是这个原因。

因此,工厂模式适用于创建少量、一次性、无需类型识别和方法复用的简单对象。对于需要创建大量实例或需要构建清晰继承关系的场景,我们需要更强大的工具。

3. 构造函数模式:赋予对象“姓氏”

为了解决工厂模式“身份不明”的问题,JavaScript提供了构造函数模式。通过new操作符调用一个普通函数,这个函数就扮演了构造器的角色,为新对象打上“家族烙印”。

3.1new操作符背后的四步魔法

当你写下const obj = new MyFunction()时,引擎在幕后默默做了四件事:

  1. 创建一个全新的空对象
  2. 将这个新对象的内部[[Prototype]](即__proto__)链接到构造函数的prototype属性指向的对象。这是实现继承的基石。
  3. 将构造函数内部的this绑定到这个新创建的对象
  4. 执行构造函数内部的代码(为this添加属性)。如果构造函数没有显式返回一个对象,则自动返回这个新创建的对象。
function Person(name, age) { // 此处的 this 指向 new 创建的新对象 this.name = name; this.age = age; this.sayName = function() { console.log(this.name); }; // 没有 return 语句,默认返回 this } const person1 = new Person('王五', 28); const person2 = new Person('赵六', 35); console.log(person1 instanceof Person); // true!解决了身份问题 console.log(person1.constructor === Person); // true

现在,person1可以明确地被识别为Person的实例,这为代码的组织和调试带来了极大的便利。

3.2 方法定义的内存陷阱与变通方案

构造函数模式解决了“身份”问题,但它没有解决“方法复用”的问题。上面代码中,sayName方法仍然是在每个实例中被重新创建。person1.sayName === person2.sayName的结果是false

为了解决这个问题,一个常见的变通方案是将方法定义转移到构造函数外部:

function sayName() { console.log(this.name); } function Person(name, age) { this.name = name; this.age = age; this.sayName = sayName; // 引用外部同一个函数 } const p1 = new Person('小明', 10); const p2 = new Person('小红', 12); console.log(p1.sayName === p2.sayName); // true!内存优化了

这样做确实实现了方法的共享,但带来了新的问题:污染了全局命名空间。如果有很多构造函数,每个都有若干方法,全局作用域下就会充斥着大量函数,极易引发命名冲突,且代码组织混乱,不符合高内聚的原则。

我们需要一种机制,既能将方法共享给所有实例,又能将这些方法优雅地组织在构造函数名下。这就是原型模式登场的时候。

4. 原型模式:共享的“家族宝藏”

在JavaScript中,每个函数都有一个prototype(原型)属性,它是一个对象。当通过new调用这个函数创建实例时,该实例的内部[[Prototype]]会指向这个原型对象。原型模式的核心思想是:将所有实例需要共享的属性和方法,直接定义在构造函数的原型对象上

4.1 理解原型链与属性查找机制

function Person() {} // 空构造函数 // 在原型上添加共享属性和方法 Person.prototype.name = '默认姓名'; Person.prototype.age = 0; Person.prototype.friends = ['张三', '李四']; Person.prototype.sayName = function() { console.log(this.name); }; const person1 = new Person(); const person2 = new Person(); person1.sayName(); // 输出:默认姓名 console.log(person1.sayName === person2.sayName); // true!方法完美共享

当我们访问person1.sayName时,引擎首先在person1实例自身查找sayName属性,没找到。接着,它会沿着person1[[Prototype]](即Person.prototype)向上查找,在那里找到了,于是调用它。这条查找路径就是原型链

4.2 共享引用类型属性带来的“惊天大坑”

原型模式在共享方法上表现完美,但在共享引用类型的属性时,却可能引发灾难性的后果。

person1.name = '王小二'; // 在实例上添加同名属性,遮蔽原型属性 console.log(person1.name); // '王小二' (来自实例) console.log(person2.name); // '默认姓名' (来自原型) // 问题来了:修改共享的引用类型属性 person1.friends.push('王五'); console.log(person1.friends); // ['张三', '李四', '王五'] console.log(person2.friends); // ['张三', '李四', '王五']!person2的也被改了

friends是一个数组,存在于Person.prototype上。person1person2访问的是同一个数组。通过person1修改这个数组,person2看到的也会同步变化。这几乎从来都不是我们想要的效果!我们希望每个实例有自己独立的friends列表。

踩坑实录:早期做一个多人协作的TODO List原型时,我使用了原型模式来存储每个用户的待办事项数组。结果一个用户添加的任务,神奇地出现在了所有用户的列表里,造成了严重的逻辑错误。排查了半天,才意识到是原型上的引用类型属性在作祟。

因此,纯原型模式适用于方法共享,但属性(尤其是引用类型属性)必须独立的场景。这引出了最经典、最常用的组合模式。

5. 组合使用构造函数和原型模式:经典的王道组合

这是ES6类语法出现之前,使用最广泛、认可度最高的创建自定义类型的方式。它完美融合了构造函数模式和原型模式的优点,同时规避了它们的缺点。

5.1 模式结构与最佳实践

思路非常简单清晰:

  • 使用构造函数模式来定义实例属性。这些属性通常是每个实例独有的基本值或引用值。
  • 使用原型模式来定义方法和需要共享的属性
// 1. 构造函数定义实例属性 function Person(name, age, friends) { this.name = name; // 实例自有 this.age = age; // 实例自有 this.friends = friends || []; // 实例自有,默认空数组,互不干扰 } // 2. 原型定义共享方法 Person.prototype.sayName = function() { console.log(this.name); }; Person.prototype.addFriend = function(friendName) { this.friends.push(friendName); // 操作的是实例自身的 friends }; // 使用 const p1 = new Person('小明', 10, ['小刚']); const p2 = new Person('小红', 12, ['小芳']); p1.addFriend('小强'); console.log(p1.friends); // ['小刚', '小强'] console.log(p2.friends); // ['小芳'],互不影响! console.log(p1.sayName === p2.sayName); // true,方法共享

这种组合方式,实例属性在构造函数中初始化,确保了数据的独立性;共享方法存放在原型上,确保了内存的高效利用。它同时满足了类型识别 (instanceof)、内存效率和数据封装的需求。

5.2 动态原型模式:更优雅的封装变体

经典组合模式有一个小小的“美学”问题:构造函数的定义和原型方法的定义在代码上是分离的。为了让代码封装得更紧密,我们可以使用“动态原型模式”。

function Person(name, age) { // 实例属性 this.name = name; this.age = age; this.friends = []; // 方法:仅在第一次调用构造函数时,添加到原型上 if (typeof this.sayName !== 'function') { Person.prototype.sayName = function() { console.log(this.name); }; Person.prototype.addFriend = function(friendName) { this.friends.push(friendName); }; // ... 可以继续添加其他共享方法 } }

if判断确保了原型方法只被初始化一次。无论创建多少个实例,原型赋值代码只会在第一个实例创建时执行。这让整个“类”的定义都封装在了构造函数内部,代码组织更整洁。不过在实际项目中,经典组合模式因其极致的简单和清晰,仍然是最主流的选择。

6. ES6类语法:更现代的“语法糖”

ES6引入的class关键字,并没有引入新的面向对象继承模型,它只是上述组合模式(构造函数+原型)的语法糖,但让代码的书写和阅读都更加清晰、更接近传统面向对象语言。

6.1 类语法基本结构与原理对应

让我们用class重写上面的Person

class Person { // 构造函数,对应原来的构造函数函数体 constructor(name, age) { this.name = name; // 实例属性 this.age = age; this.friends = []; } // 类方法,会自动添加到 Person.prototype 上 sayName() { console.log(this.name); } addFriend(friendName) { this.friends.push(friendName); } // 静态方法,属于类本身,而不是实例 static describe() { return '这是一个Person类'; } } // 使用完全一样 const p1 = new Person('小明', 10); p1.sayName(); // 小明 console.log(p1.sayName === Person.prototype.sayName); // true // 调用静态方法 console.log(Person.describe()); // 这是一个Person类

可以看到:

  • constructor方法就是原来的构造函数。
  • 在类块中定义的方法,默认就是原型方法。
  • 使用static关键字可以定义静态方法,这是以前需要直接挂在构造函数上(如Person.describe = function(){})才能实现的功能,现在语法更统一。

6.2 类语法的优势与细节陷阱

优势:

  1. 语法简洁直观:不再需要手动操作prototype,继承语法(extends)也极其简单。
  2. 更好的内置检查:必须使用new调用,否则报错。而传统的构造函数如果忘了newthis会指向全局对象(严格模式下为undefined),导致难以追踪的错误。
  3. 支持继承的super关键字,调用父类构造函数和方法更规范。
  4. 清晰的静态成员定义

需要注意的细节:

  • 类定义不会被提升。虽然函数声明会提升,但class不会。你不能在定义类之前使用它。
  • 类体内的代码默认在严格模式下执行
  • 所有方法都是不可枚举的。而在传统原型模式中,我们添加到prototype上的方法是可枚举的(可以通过for...in遍历到)。这通常更符合预期。
  • 类没有真正的“私有属性”(ES2022引入了#前缀的私有字段,但属于较新特性)。在构造函数中定义的属性仍然是公开的。

6.3 如何选择:类语法 vs 经典组合模式?

在现代前端开发中(React, Vue 3等),class语法已经成为绝对的主流,因为它更清晰、更现代,并且被框架和工具链良好支持。

我个人的建议是:在新项目中,毫不犹豫地使用class语法。它解决了旧模式的大部分痛点,并且是语言标准的发展方向。

然而,理解其背后的原型机制(经典组合模式)依然无比重要:

  1. 调试需要:当你的代码在原型链上出现问题时,如果你只懂class语法,调试将非常困难。你必须能看懂__proto__prototype
  2. 理解旧代码:大量的遗留库和项目仍然使用传统的模式。
  3. 应对特殊场景:极少数情况下,你可能需要直接操作原型对象来实现一些高级技巧(比如猴子补丁),这时对原型的深刻理解是必不可少的。

7. 五种方式对比与实战选型指南

让我们通过一个表格,从核心特征、优缺点和典型场景来快速回顾这五种方式:

创建方式核心特征优点缺点适用场景
工厂模式函数返回新对象,无new简单,封装创建过程1. 对象无法类型识别 (instanceof)
2. 方法无法共享,内存浪费
创建少量、简单、无需类型检查的配置对象或数据模型
构造函数模式使用new调用函数,this指向新对象1. 解决了类型识别问题
2. 实例属性独立
1. 方法仍未共享,内存浪费
2. 全局定义方法污染命名空间
需要明确类型,但实例数量极少,或方法复杂度极低的场景(现已很少单独使用)
原型模式将共享成员定义在构造函数的prototype1. 方法完美共享,内存高效
2. 类型识别正常
1. 所有实例共享原型上的引用类型属性,导致数据污染
2. 无法在创建时为实例初始化独立的属性值
几乎不单独使用,主要用于方法共享部分的实现
组合模式实例属性在构造函数中定义,共享方法在原型上定义1. 实例属性独立
2. 方法共享,内存高效
3. 类型识别正常
4. 是ES6类的底层原理
代码组织上,构造函数和原型定义略显分离(可用动态原型模式优化)ES6之前创建自定义类型的标准、主流方式,理解它才能深入理解JS对象系统
ES6类class语法糖,对应组合模式1. 语法简洁现代,封装性好
2. 内置严格模式和new检查
3. 继承语法 (extends) 清晰
4. 是未来标准
1. 类定义不提升
2. 需要理解其背后的原型原理才能深度调试
现代JS开发的默认选择,适用于所有需要创建自定义类型、构建复杂应用的场景

实战选型心法:

  • 如果你在写现代项目(ES6+),直接使用class。这是最安全、最主流、最面向未来的选择。
  • 如果你需要维护或理解旧代码库,必须熟练掌握组合模式,因为它是class的基石,也是旧代码的常态。
  • 当你需要创建一个纯粹的数据容器,且不关心它的“类型”时,可以考虑简单的工厂函数或直接使用对象字面量{}
  • 永远避免单独使用纯构造函数模式或纯原型模式,它们各自的问题在组合模式/类语法中都已得到解决。

最后,无论选择哪种方式,关键是要理解其背后的内存模型和设计取舍。JavaScript的对象系统灵活而强大,这种灵活性既是其魅力所在,也要求开发者必须具备更扎实的理解,才能写出高效可靠的代码。在我多年的开发经历中,见过太多因为错误使用原型导致的数据共享bug,也见过因为不理解newthis绑定而引发的诡异错误。希望这次对五种创建方式的深度拆解,能帮你建立起清晰的知识图谱,在下次创建对象时,能自信地做出最适合当前场景的选择。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/3 10:13:50

Java超市购物系统实战:Spring Boot+MyBatis-Plus构建与核心业务实现

简介:在软件开发领域,数据库设计与业务逻辑实现是构建健壮应用的核心基础。其原理在于通过合理的表结构规划与事务管理,确保数据一致性并支撑复杂业务场景。从技术价值看,这不仅关乎功能实现,更直接影响系统的可维护性…

作者头像 李华
网站建设 2026/9/2 8:13:17

C# 多个串口多个线程发送数据和接收数据

目录 1. 创建串口对象 2. 创建线程用于发送和接收数据 使用Thread类 使用Task类 3. 启动线程/任务并管理资源 4. 优雅地关闭串口和线程/任务(可选) 5. 处理异常和错误(可选) 如果您喜欢此文章,请收藏、点赞、评…

作者头像 李华
网站建设 2026/9/2 10:37:02

蓝桥杯国赛单片机代码深度解析:模块化、状态机与工程实践

1. 项目概述:从国赛真题到实战代码的深度复盘最近在整理过往的竞赛资料,翻到了第十一届蓝桥杯单片机设计与开发大学组国赛的代码。这不仅仅是一份代码,更像是一份浓缩了特定时期技术挑战、设计思路和临场应对策略的“考古”样本。蓝桥杯的单片…

作者头像 李华
网站建设 2026/9/1 12:23:22

Coze智能体搭建全指南:从0基础入门到企业级工作流实战

Coze(扣子)是目前搭建 AI 智能体绕不开的平台,尤其当你需要把大模型、知识库、插件、工作流和多端发布串在一起时,它能省掉大量从零开发的工作。很多教程会把 Coze 讲得很玄,实际上核心就三件事:搭建智能体…

作者头像 李华
网站建设 2026/9/1 13:15:44

基于springboot+vue跨境电商管理系统的设计与实现(源码+讲解视频+LW)

温馨提示:本人主页置顶文章(点我)开头有 CSDN 平台官方提供的学长联系方式的名片! 温馨提示:本人主页置顶文章(点我)开头有 CSDN 平台官方提供的学长联系方式的名片! 温馨提示:本人主页置顶文章(点我)开头有 CSDN 平台…

作者头像 李华
网站建设 2026/8/31 2:52:49

YOLOv8校园能耗行为识别系统实战指南

简介:目标检测是计算机视觉的基础任务,其核心在于从图像中准确定位并分类感兴趣对象;YOLO系列模型凭借端到端、高效率的特性,成为轻量级部署场景的首选。在智慧校园建设中,将目标检测技术落地为设备状态识别系统&#…

作者头像 李华