尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

JavaScript对象创建模式:从工厂、构造函数到原型与组合模式

JavaScript对象创建模式:从工厂、构造函数到原型与组合模式 1. 从“面条代码”到模块化为什么我们需要封装如果你写过一段时间的JavaScript尤其是接手过一些“祖传”代码大概率见过这样的场景几百行代码挤在一个文件里变量名是a、b、c函数之间互相调用改一个地方可能引发三个未知的错误。这种代码通常被称为“面条代码”Spaghetti Code其可读性、可维护性和复用性几乎为零。封装就是解决这个问题的第一把钥匙。它不是什么高深莫测的玄学而是一种最朴素的编程思想将数据和操作数据的方法捆绑在一起对外隐藏内部复杂的实现细节只暴露必要的接口。想象一下你用的电视机你不需要知道里面复杂的电路板如何工作只需要知道按哪个按钮能开机、换台、调音量。电视机内部电路对你而言就是“封装”起来的。在JavaScript这个灵活到有些“松散”的语言里封装尤为重要。因为它没有Java、C#那种语言级别的class和private关键字ES6的class本质是语法糖#私有字段是后来才加入的早期的封装全靠开发者的“约定”和“模式”来实现。这就催生了工厂模式、构造函数模式、原型模式这些经典的创建对象模式。它们不仅仅是创建对象的不同写法更体现了JavaScript面向对象编程思想的演进脉络——从模仿到形成自己的特色。很多人学这些模式只记住了代码怎么写却忽略了它们各自要解决的核心问题以及背后的权衡。今天我们就抛开教科书式的定义从“为什么要用”和“实际怎么用”的角度把这三种模式的封装掰开揉碎了讲清楚并附上可以直接“抄作业”的代码实现和避坑指南。2. 工厂模式快速批量的对象“作坊”工厂模式可以理解为最直观的一种封装方式。它的核心思想很简单定义一个函数工厂这个函数负责接收原料参数按照固定的流程逻辑生产出产品对象然后返回这个产品。2.1 核心动机告别重复的new Object()在工厂模式出现之前如果我们想创建多个结构相似的对象可能会这样写const person1 { name: 小明, age: 20, sayName: function() { console.log(this.name); } }; const person2 { name: 小红, age: 22, sayName: function() { console.log(this.name); } };这带来了两个明显问题1. 代码重复每个对象都要写一遍属性和方法2.sayName方法在每个对象里都存了一份浪费内存。工厂模式就是为了解决这种重复劳动而生的。2.2 标准实现与代码解析一个典型的工厂函数如下所示function createPerson(name, age) { // 1. 创建一个新对象准备原材料 const obj new Object(); // 2. 为这个对象添加属性和方法加工组装 obj.name name; obj.age age; obj.sayName function() { console.log(this.name); }; // 3. 返回加工好的对象出厂 return obj; } // 使用工厂 const person1 createPerson(小明, 20); const person2 createPerson(小红, 22); person1.sayName(); // 输出小明 console.log(person1 instanceof Object); // true console.log(person1.constructor); // [Function: Object]逐行拆解与思考const obj new Object();这是最基础的对象创建方式。在函数内部创建对象将创建细节封装了起来。属性和方法的赋值将传入的参数绑定到对象上。注意这里每个对象的sayName方法都是全新的函数即使它们功能一模一样。return obj;这是工厂模式的关键。通过返回值将创建好的对象交给调用者。2.3 工厂模式的优缺点与适用场景优点简单直观逻辑清晰容易理解和实现。解耦将对象的创建与使用分离。调用者无需关心对象是如何被组装的只需知道“工厂”能生产什么。灵活性可以在工厂函数内部根据参数进行复杂的逻辑判断返回不同类型的对象。致命缺点对象类型识别问题这是工厂模式最大的痛点。所有通过工厂创建的对象其构造函数都指向顶层的Object而不是createPerson。person1 instanceof createPerson会返回false。这在需要精确判断对象来源的场景下比如插件系统、错误追踪非常不便。内存浪费如前所述每个对象都拥有自己独立的方法副本。创建100个对象就有100个功能相同的sayName函数在内存中这是极大的资源浪费。适用场景当你需要快速创建大量简单、轻量、且不需要复杂类型识别和继承关系的对象时。适用于一些工具类对象的创建其生命周期短且更关注数据本身而非行为。作为学习理解封装概念的入门范例非常合适但在现代稍具规模的JavaScript项目中单纯使用工厂模式的情况已经比较少了。注意虽然工厂模式有缺点但它体现的“封装创建过程”的思想被广泛用于更高阶的设计模式中如抽象工厂、建造者模式。理解它是理解更复杂模式的基础。3. 构造函数模式赋予对象“身份证”为了解决工厂模式“对象类型无法识别”的问题JavaScript提供了构造函数模式。这不仅仅是语法上的变化更是一种理念的升级让创建的对象知道自己“是谁生的”。3.1new操作符背后的四步魔法构造函数本质上就是一个普通函数但通常约定函数名首字母大写。它的魔力来自于new操作符。当你使用new Constructor()时后台默默做了四件事创建一个全新的空对象。将这个新对象的内部[[Prototype]]即__proto__链接到构造函数的prototype对象。这是实现继承的关键一步。将构造函数内部的this绑定到这个新创建的对象。如果构造函数没有显式返回一个对象则自动返回这个新创建的对象。理解了这四步就能看透构造函数模式的本质。3.2 标准实现与内存问题剖析让我们用构造函数重写上面的例子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(小明, 20); const person2 new Person(小红, 22); person1.sayName(); // 输出小明 console.log(person1 instanceof Person); // true问题解决了 console.log(person1.constructor); // [Function: Person]关键进步现在person1 instanceof Person返回true我们可以明确知道person1是由Person“构造”出来的。对象的“身份证”问题解决了。遗留的顽疾方法的内存浪费然而喜悦是短暂的。我们仔细看this.sayName function() { ... }这一行依然在每次调用new Person()时都会创建一个全新的函数对象并赋值给新实例的sayName属性。person1.sayName person2.sayName的结果是false。内存浪费的问题和工厂模式一模一样甚至更糟因为它给每个实例都打上了“我是Person”的标签但内部却做着重复存储的事。3.3 进阶尝试将方法移到构造函数外部一个直观的优化思路是把函数定义移到构造函数外面构造函数内部只进行引用。function sayName() { console.log(this.name); } function Person(name, age) { this.name name; this.age age; this.sayName sayName; // 引用外部函数 } const person1 new Person(小明, 20); const person2 new Person(小红, 22); console.log(person1.sayName person2.sayName); // true内存问题解决这样做确实解决了内存问题所有实例共享同一个sayName函数。但它带来了新的问题污染全局命名空间sayName函数暴露在全局可能与其他代码冲突。封装性被破坏从代码组织上看sayName本应是Person的“私有”行为现在却独立在外破坏了对象的封装性。如果有很多方法全局就会有很多函数难以管理。构造函数模式解决了类型识别但优雅地解决共享方法的问题需要引出更强大的机制——原型。4. 原型模式共享的“家族宝藏”JavaScript中每个函数都有一个特殊的属性prototype原型属性。它是一个对象。当使用new调用构造函数创建实例时该实例的内部[[Prototype]]会指向构造函数的prototype对象。原型模式的核心就是利用这个链接让所有实例共享原型对象上的属性和方法。4.1 理解原型链属性的查找机制当你访问一个对象的属性比如person1.sayName时JavaScript引擎会执行以下搜索首先在对象实例本身查找是否有该属性。有则返回搜索停止。如果没有则沿着实例的__proto__指针到它的原型对象即Person.prototype上查找。如果原型对象上还没有则继续沿着原型对象的__proto__向上查找原型对象也是对象它也有原型直到找到Object.prototype如果还没有则返回undefined。这条搜索路径就是原型链。原型模式正是基于这个机制。4.2 标准实现将方法定义在原型上让我们用原型模式彻底改造Person// 1. 定义构造函数只初始化实例独有的属性 function Person(name, age) { this.name name; // 实例属性 this.age age; // 实例属性 } // 2. 将共享的方法添加到构造函数的 prototype 对象上 Person.prototype.sayName function() { console.log(this.name); }; // 3. 还可以添加共享的属性需谨慎 Person.prototype.species Homo sapiens; const person1 new Person(小明, 20); const person2 new Person(小红, 22); person1.sayName(); // 输出小明 console.log(person1.sayName person2.sayName); // true完美共享 console.log(person1.species); // Homo sapiens console.log(person2.species); // Homo sapiens优势分析极致的内存效率sayName方法只在内存中存在一份所有Person实例通过原型链共享它。强大的封装性方法被很好地组织在Person.prototype这个与构造函数关联的对象下没有污染全局空间。保留类型识别instanceof和constructor判断依然有效。动态性即使在创建实例之后我们修改Person.prototype所有已存在的实例也能立即“看到”这个变化因为查找是动态的。4.3 原型模式的陷阱与最佳实践原型模式非常强大但使用不当也会踩坑。陷阱一重写原型对象会切断已有实例的联系function Person() {} const person1 new Person(); // 最初的原型上有一个方法 Person.prototype.sayHi function() { console.log(Hi); }; person1.sayHi(); // 输出Hi // 错误做法完全重写 prototype 对象 Person.prototype { sayHello: function() { console.log(Hello); } }; const person2 new Person(); person2.sayHello(); // 输出Hello person2.sayHi(); // 报错person2.sayHi is not a function person1.sayHi(); // 输出Hi person1依然链接到旧的原型 person1.sayHello(); // 报错person1.sayHello is not a functionperson1的[[Prototype]]仍然指向最初的那个原型对象。重写Person.prototype只是让构造函数指向了一个新的原型对象不影响已创建的实例。新创建的person2则指向新的原型对象。这导致了混乱。最佳实践永远通过Constructor.prototype.methodName ...的形式来添加属性或方法避免直接用一个新对象覆盖prototype。如果非要覆盖必须在创建任何实例之前完成。陷阱二原型上的引用类型属性会被所有实例共享这是原型模式最著名的“坑”。function Person() {} Person.prototype.friends [Alice, Bob]; // 引用类型属性 Person.prototype.sayFriends function() { console.log(this.friends); }; const person1 new Person(); const person2 new Person(); person1.friends.push(Charlie); console.log(person1.friends); // [Alice, Bob, Charlie] console.log(person2.friends); // [Alice, Bob, Charlie]person2的也被改了因为person1.friends和person2.friends指向的是同一个数组原型上的那个。修改其中一个必然影响另一个。最佳实践将需要独立维护的属性尤其是引用类型定义在构造函数内部作为实例属性。只有纯函数、常量或真正需要共享的配置项才适合放在原型上。 修正后的写法function Person() { this.friends [Alice, Bob]; // 每个实例拥有独立的数组 } Person.prototype.sayFriends function() { console.log(this.friends); };5. 组合模式实践中最常用的“黄金法则”经过前面的分析我们发现没有一种模式是完美的工厂模式类型识别难内存浪费。构造函数模式解决了类型但没解决内存。原型模式解决了内存和类型但共享引用属性是坑。于是在实践中一个结合了构造函数模式和原型模式优点的“组合模式”成为了最广泛使用的默认选择。其规则非常简单使用构造函数模式定义实例属性使用原型模式定义共享的方法和常量属性。5.1 标准组合模式实现这就是目前最经典、最推荐的JavaScript对象创建模式。// 1. 构造函数定义每个实例独有的属性 function Person(name, age, job) { this.name name; this.age age; this.job job; this.friends [Alice, Bob]; // 引用类型也放在这里每个实例独立 } // 2. 原型定义所有实例共享的方法 Person.prototype { constructor: Person, // 显式指回构造函数保持完整性 sayName: function() { console.log(this.name); }, sayJob: function() { console.log(My job is ${this.job}.); } }; // 使用 const person1 new Person(Nicholas, 29, Software Engineer); const person2 new Person(Greg, 27, Doctor); person1.friends.push(Van); console.log(person1.friends); // [Alice, Bob, Van] console.log(person2.friends); // [Alice, Bob] 互不影响 console.log(person1.sayName person2.sayName); // true 共享方法 console.log(person1 instanceof Person); // true console.log(person1.constructor Person); // true5.2 为什么这是“黄金法则”内存高效方法只在原型上存在一份无论创建多少实例。实例独立每个实例拥有自己的一份属性副本特别是引用类型属性互不干扰。类型清晰instanceof和constructor都能正确工作。封装良好属性和方法被清晰地组织在构造函数和原型中代码结构一目了然。这种模式是如此成功以至于ECMAScript 5的Object.create()和ECMAScript 6的class语法其底层思想都与之高度一致。ES6的class可以看作是这种组合模式的语法糖它让代码看起来更接近传统面向对象语言但本质没变。5.3 从组合模式到ES6 Class理解了组合模式再看ES6的class就豁然开朗了。class Person { // 构造函数对应之前的构造函数模式部分 constructor(name, age, job) { this.name name; this.age age; this.job job; this.friends [Alice, Bob]; } // 类方法对应之前的原型模式部分 sayName() { console.log(this.name); } sayJob() { console.log(My job is ${this.job}.); } } // 使用完全一样 const person1 new Person(Nicholas, 29, Software Engineer);class中的constructor就是原来的构造函数class中定义的方法会自动挂载到原型上。它没有引入新的面向对象模型只是让组合模式的写法更优雅、更不易出错比如自动处理prototype.constructor的指向。6. 封装模式的演进与选择指南回顾这三种模式其实是一条清晰的演进路径工厂模式解决了“创建对象”的封装问题但对象没有“根”。构造函数模式通过new和this赋予了对象“身份”类型识别但共享能力不足。原型模式通过原型链实现了属性和方法的完美共享但需要警惕引用共享的陷阱。组合模式构造函数原型取二者之长成为事实标准。ES6 Class组合模式的语法糖提供更现代、更安全的书写方式。在实际项目中的选择建议对于现代项目ES6毫不犹豫地使用class。它清晰、安全、符合潮流是官方推荐的语法。Babel等工具会帮你处理好兼容性问题。对于需要兼容极老环境或学习底层原理深入理解组合模式。它是class的基石能帮你透彻理解this、原型链、new等核心概念。工厂模式在需要根据复杂条件创建不同类型对象、或不想暴露构造函数时仍有其用武之地。例如一个创建不同UI组件Button, Input, Modal的函数可以根据传入的type返回不同的对象调用者无需关心具体的构造函数。纯原型模式单独使用的情况较少通常在与Object.create()结合进行原型式继承时会出现。理解这些模式最终目的不是为了在写代码时纠结用哪一个而是为了在阅读任何JavaScript代码无论是古老的库还是现代的框架时都能一眼看穿其对象组织的套路在遇到诡异bug时比如为什么修改了这个数组另一个对象也变了能迅速定位到是否是原型共享引用类型惹的祸。这才是封装思想带给我们的真正价值——写出更清晰、更健壮、更易于协作的代码。
返回列表