JavaScript对象创建模式演进:从工厂函数到Class语法糖
2026/8/29 1:42:16 网站建设 项目流程

1. 从零到一:我们为什么需要“对象”?

如果你写过几行JavaScript,大概率已经和对象打过交道了。比如,你想描述一个用户,可能会随手写下{name: ‘小明‘, age: 18}。这个花括号包裹的键值对集合,就是JS中最基础的对象。它直观、好用,就像一个小袋子,能把描述同一个事物的信息装在一起。

但问题很快就来了。当你的应用里需要成百上千个用户时,难道要手动写上千个这样的字面量吗?更麻烦的是,每个用户可能都需要一些共同的行为,比如“打招呼”。你可能会在每个对象里都加一个sayHi函数,但这会造成巨大的内存浪费,因为每个函数都是独立的副本。这时候,你就会本能地思考:有没有一种方法,能像工厂流水线一样,“批量生产”结构相同、行为一致的对象呢?

这就是面向对象编程(OOP)在JavaScript中要解决的核心问题。它不仅仅是一种语法,更是一种组织代码、管理复杂度的思维方式。今天,我们就从一个最简单的需求出发,看看在JS中创建对象是如何一步步从“手工作坊”进化到“现代工厂”的。这个过程,恰恰是理解JS对象与类本质的最佳路径。

2. 创建对象的“进化史”:从工厂模式到构造函数

2.1 起点:简单粗暴的对象字面量

一切从最直接的方式开始。假设我们要创建几个表示人的对象。

const person1 = { name: ‘张三‘, age: 30, job: ‘工程师‘, sayName: function() { console.log(this.name); } }; const person2 = { name: ‘李四‘, age: 25, job: ‘设计师‘, sayName: function() { console.log(this.name); } };

优点:极其简单明了,一眼就能看懂对象的结构。致命缺点

  1. 代码冗余:每个属性都要重复写一遍,sayName方法在每个对象里都重新定义了一次。
  2. 毫无辨识度:我们无法一眼看出person1person2是由同一个“模子”创建的,它们之间没有内在联系。

注意:在只有两三个对象时,这种方式无可厚非。但一旦数量增多或结构变复杂,它就会立刻成为维护的噩梦。记住一个原则:重复的代码是“代码坏味道”的第一信号。

2.2 第一次进化:工厂模式

为了解决重复劳动的问题,我们很自然地会想到用一个函数来“生产”对象。这就是工厂模式。

function createPerson(name, age, job) { const o = new Object(); // 或者直接用 {} o.name = name; o.age = age; o.job = job; o.sayName = function() { console.log(this.name); }; return o; } const person1 = createPerson(‘张三‘, 30, ‘工程师‘); const person2 = createPerson(‘李四‘, 25, ‘设计师‘);

优点

  • 封装了创建过程:我们不用再关心对象内部如何构建,只需传入参数。
  • 解决了代码重复:对象的创建逻辑只有一份。

依然存在的问题

  1. 对象类型识别问题person1person2通过instanceof检查时,都指向Object,而不是我们期望的Person。我们无法知道它们是由哪个“工厂”创建的。
  2. 方法重复定义:每个对象仍然拥有自己独立的sayName方法,内存占用问题没有解决。

工厂模式像是一个没有商标的加工厂,能生产出合格的产品,但产品上没有任何生产厂家的印记。

2.3 关键跨越:构造函数模式

JavaScript 为我们提供了一个专门的“模具”——构造函数。它本质上就是一个普通函数,但调用方式不同。

function Person(name, age, job) { // 构造函数内部的 this 指向新创建的对象实例 this.name = name; this.age = age; this.job = job; this.sayName = function() { console.log(this.name); }; // 没有 return 语句 } // 使用 new 操作符调用 const person1 = new Person(‘张三‘, 30, ‘工程师‘); const person2 = new Person(‘李四‘, 25, ‘设计师‘); console.log(person1 instanceof Person); // true console.log(person1 instanceof Object); // true console.log(person1.constructor === Person); // true

这里发生了四件关键事情:

  1. 在内存中创建一个新对象。
  2. 这个新对象内部的[[Prototype]](即__proto__)特性被赋值为构造函数的prototype属性。
  3. 构造函数内部的this被赋值为这个新对象。
  4. 执行构造函数内部的代码(给新对象添加属性)。
  5. 如果构造函数没有显式返回其他对象,则自动返回这个新对象。

划时代的优点

  • 类型识别instanceofconstructor属性都能正确标识对象的“出身”,解决了工厂模式的最大痛点。
  • 约定俗成:构造函数名首字母大写,这是一种强烈的视觉信号,提醒开发者此函数需要用new调用。

遗留的顽疾

  • 方法依然不共享person1.sayName === person2.sayName的结果是false。创建100个Person实例,就会在内存中存在100个功能完全相同的sayName函数,这是极大的浪费。

实操心得:忘记使用new操作符是初学者常犯的错误。如果像普通函数一样调用Person(),那么函数内部的this在非严格模式下会指向全局对象(如window),造成属性泄露到全局,这是非常危险的Bug。一些现代库或框架会通过检查this是否是构造函数的实例,来强制要求使用new,或者使用 ES6 的new.target来检测。

2.4 性能优化:原型模式

我们终于要直面“方法共享”这个核心问题了。JavaScript 的每个函数都有一个prototype(原型)属性,它是一个对象。当使用构造函数创建实例时,所有实例内部都有一个指针([[Prototype]],可通过__proto__访问)指向这个原型对象。

核心思想:把所有实例需要共享的属性和方法,直接定义在构造函数的prototype对象上。

function Person(name, age, job) { // 实例属性:每个实例独有的数据 this.name = name; this.age = age; this.job = job; } // 原型方法:所有实例共享的行为 Person.prototype.sayName = function() { console.log(this.name); }; // 也可以在原型上定义共享的属性(通常是不变的常量) Person.prototype.species = ‘人类‘; const person1 = new Person(‘张三‘, 30, ‘工程师‘); const person2 = new Person(‘李四‘, 25, ‘设计师‘); console.log(person1.sayName === person2.sayName); // true!方法实现了共享 console.log(person1.species); // ‘人类‘

工作原理(查找机制): 当访问一个对象的属性(如person1.sayName)时,JS引擎会执行以下搜索:

  1. 首先在对象实例本身查找是否有该属性。
  2. 如果没有,则沿着[[Prototype]]指针去它的原型对象上查找。
  3. 如果原型对象上还没有,就继续去原型的原型上找,直到找到Object.prototype(所有对象的顶层原型)为止。这就是原型链

优点

  • 极致的内存效率:无论创建多少实例,共享方法在内存中只存在一份。
  • 动态性:即使实例已经创建,我们仍然可以通过修改Person.prototype来为所有实例添加新的共享方法。

缺点与注意事项

  1. 共享引用类型属性的陷阱:这是原型模式最大的坑。如果原型属性是一个引用类型(如数组、对象),那么所有实例将共享同一个引用。

    function Person() {} Person.prototype.friends = [‘小王‘, ‘小李‘]; // 引用类型属性 const p1 = new Person(); const p2 = new Person(); p1.friends.push(‘小张‘); console.log(p2.friends); // [‘小王‘, ‘小李‘, ‘小张‘]!p2的friends也被修改了

    避坑指南永远不要将需要独立维护的引用类型属性放在原型上。实例特有的属性,务必在构造函数内部用this.xxx来定义。

  2. 无法通过实例重写原型person1.prototypeundefined。实例只能访问原型,不能直接修改构造函数的原型引用。

  3. 参数传递问题:纯原型模式无法在创建实例时差异化地初始化属性。因此,它通常不单独使用。

2.5 终极组合:组合构造函数与原型模式

这是ES6之前,在JavaScript中创建自定义引用类型的最主流、最广泛认可的模式。它完美结合了构造函数模式和原型模式的优点。

设计原则

  • 构造函数内:定义实例属性。这些是每个实例独有的数据,特别是引用类型属性。
  • 原型上:定义共享的方法和常量属性。所有实例共享行为,节省内存。
// 1. 在构造函数中定义实例属性 function Person(name, age, job) { this.name = name; this.age = age; this.job = job; this.friends = [‘小王‘, ‘小李‘]; // 引用类型,每个实例独立 } // 2. 在原型上定义共享方法 Person.prototype = { constructor: Person, // 显式指回构造函数,保持一致性 sayName: function() { console.log(this.name); }, sayJob: function() { console.log(`我的工作是${this.job}`); } }; // 使用 const p1 = new Person(‘张三‘, 30, ‘工程师‘); const p2 = new Person(‘李四‘, 25, ‘设计师‘); p1.friends.push(‘小张‘); console.log(p1.friends); // [‘小王‘, ‘小李‘, ‘小张‘] console.log(p2.friends); // [‘小王‘, ‘小李‘] 互不影响 console.log(p1.sayName === p2.sayName); // true 方法共享

为什么这是“终极”模式?

  1. 内存高效:方法只创建一次,供所有实例使用。
  2. 实例独立:每个实例拥有自己的属性副本,特别是引用类型属性,互不干扰。
  3. 类型识别清晰instanceofconstructor都能正确工作。
  4. 参数化创建:可以通过构造函数传递参数,灵活初始化每个实例。

这个模式是如此经典和有效,以至于ES6的class语法糖,其底层实现思想就是对此模式的官方标准化和封装。

深度解析:为什么需要constructor: Person这一行?当我们用对象字面量{}完全重写Person.prototype时,我们实际上创建了一个新的对象。这个新对象的constructor属性会指向Object构造函数,而不是Person。这会导致p1.constructor === Person返回false,破坏了类型识别。虽然不影响instanceof的工作,但为了保持完整性,最好显式地将constructor指回来。如果你是通过Person.prototype.sayName = ...的方式逐个添加方法,则不会覆盖原有的constructor属性。

3. 其他创建模式:理解它们的适用场景与局限

在追求“完美”模式的道路上,开发者们还探索过一些变体。了解它们有助于你更深刻地理解JavaScript对象的灵活性。

3.1 动态原型模式

组合模式有一个“小瑕疵”:定义被拆分在了两个地方(构造函数内部和外部)。动态原型模式旨在将所有信息都封装在构造函数内,保持更好的封装性。

function Person(name, age, job) { // 属性 this.name = name; this.age = age; this.job = job; this.friends = [‘小王‘, ‘小李‘]; // 方法 - 仅在第一次调用构造函数时初始化原型 if (typeof this.sayName !== ‘function‘) { Person.prototype.sayName = function() { console.log(this.name); }; Person.prototype.sayJob = function() { console.log(`我的工作是${this.job}`); }; // ... 可以继续添加其他共享方法 } }

原理:通过检查某个原型方法是否已存在,来决定是否需要初始化原型。new Person()时,this指向新实例,第一次检查时原型上肯定没有sayName,于是执行初始化。后续再new时,原型方法已存在,就不再重复执行初始化代码。

优点:代码组织更紧凑,所有定义都在构造函数内部。缺点:不能使用对象字面量一次性重写原型(否则会切断已有实例与新原型的联系)。对原型所做的修改,无法立即在所有实例上反映(已存在的实例的[[Prototype]]指向的还是旧的原型对象)。因此,这种模式不如组合模式直观和通用。

3.2 寄生构造函数模式

这种模式看起来像是一个工厂函数内部使用了new操作符,用于创建一个特殊类型的对象,而不修改原有构造函数。

function SpecialArray(...elements) { const array = new Array(); // 创建基础对象 array.push(...elements); // 添加特殊方法 array.toPipedString = function() { return this.join(‘|‘); }; return array; // 返回这个加工后的对象 } const colors = new SpecialArray(‘red‘, ‘blue‘, ‘green‘); console.log(colors.toPipedString()); // ‘red|blue|green‘ console.log(colors instanceof SpecialArray); // false! 它其实是Array的实例 console.log(colors instanceof Array); // true

本质:它就是一个工厂函数,只是语法上使用了new。返回的对象与构造函数SpecialArray没有原型链上的联系。使用场景:极其罕见。通常用于为一些内置类型(如Array、Date)添加额外功能,而又不想直接修改这些类型的原型(避免污染全局)。但在现代JS中,通过类继承(extends Array)是更优雅的选择。

3.3 稳妥构造函数模式

这是一种强调安全性的模式,适用于某些安全环境(如防止数据被第三方脚本篡改),它不依赖thisnew

function Person(name, age, job) { const o = new Object(); // 创建私有对象 // 可以在这里定义私有变量和函数 // 定义特权方法,用于访问私有数据 o.sayName = function() { console.log(name); // 直接访问传入的参数,而不是 this.name }; o.sayJob = function() { console.log(job); }; return o; } const friend = Person(‘李雷‘, 28, ‘医生‘); // 注意:没有使用 new friend.sayName(); // ‘李雷‘ console.log(friend.name); // undefined,因为name不是对象的属性

特点

  • 不使用new调用构造函数。
  • 不引用this
  • 实例方法只能通过闭包访问传入的原始数据,无法直接访问对象属性。
  • 创建的对象与构造函数之间也没有原型链联系。

适用场景:在一些对安全要求极高、需要防止数据被意外修改或访问的场景。在普通Web开发中极少使用。

4. 从原型到类:ES6 Class 语法糖的本质

ES6引入的class关键字,并没有给JavaScript带来新的面向对象继承模型。它只是上述组合构造函数与原型模式的语法糖,让代码更清晰、更像传统面向对象语言。

// ES6 Class 写法 class Person { constructor(name, age, job) { // 这部分对应构造函数内部的实例属性定义 this.name = name; this.age = age; this.job = job; this.friends = [‘小王‘, ‘小李‘]; } // 类中定义的方法,自动添加到 Person.prototype 上 sayName() { console.log(this.name); } sayJob() { console.log(`我的工作是${this.job}`); } // 静态方法,属于类本身,而不是实例 static describe() { console.log(‘这是一个表示人的类‘); } } // 使用 const p1 = new Person(‘韩梅梅‘, 26, ‘教师‘); p1.sayName(); // ‘韩梅梅‘ Person.describe(); // ‘这是一个表示人的类‘ // 验证其本质 console.log(typeof Person); // ‘function‘,类本质是函数 console.log(p1.__proto__ === Person.prototype); // true console.log(p1.sayName === Person.prototype.sayName); // true

Class 语法带来的核心便利

  1. 统一的书写结构:将构造逻辑和原型方法定义放在一个class块中,结构更清晰。
  2. 更直观的继承:通过extendssuper实现继承,比ES5的原型链继承写法直观太多。
  3. 内置的严格模式:类声明和类表达式中的代码默认在严格模式下执行。
  4. 静态方法和访问器属性:语法支持更完善。
  5. 不可枚举:类中定义的方法默认是不可枚举的(Object.keys(Person.prototype)拿不到),这更符合内置对象的行为。

必须牢记的“糖”

  • class只是语法糖,JavaScript的继承机制依然是基于原型的。
  • constructor方法就是原来的构造函数。
  • 类中定义的方法就是原来写在prototype上的方法。
  • 类声明不存在提升(与函数声明不同),必须先声明后使用。

5. 实战避坑与性能优化指南

理解了理论,在实际编码中还有一堆坑等着你。下面是我从多年实践中总结出的高频问题和优化建议。

5.1 原型操作中的常见“天坑”

坑1:在实例化后重写整个原型对象

function Person() {} const p1 = new Person(); // 重写原型 Person.prototype = { constructor: Person, sayHi() { console.log(‘Hi‘); } }; p1.sayHi(); // TypeError: p1.sayHi is not a function

原因p1的内部[[Prototype]]指向的是最初的那个原型对象。重写Person.prototype只是改变了构造函数的prototype属性指向一个新的对象,但已经创建的实例依然链接着旧的原型。正确做法:要么在创建任何实例之前定义好原型,要么通过Person.prototype.sayHi = ...的形式动态添加方法,而不是用新对象整体替换。

坑2:忘记使用new操作符

function Person(name) { this.name = name; } const p = Person(‘错误‘); // 忘记 new console.log(name); // ‘错误‘ (污染了全局变量) console.log(p); // undefined

防御性编程:在构造函数内部加入检查。

function Person(name) { if (!(this instanceof Person)) { // 或者 if (new.target === undefined) { (ES6) throw new Error(‘Person 必须使用 new 调用‘); // 或者 return new Person(name); // 自动纠正(工厂模式变体) } this.name = name; }

坑3:循环依赖导致的原型链断裂在复杂的继承关系中,如果父类和子类的定义存在循环引用或顺序问题,可能导致原型链无法正确建立。务必保证父类在子类之前定义。

5.2 对象创建的性能考量

  1. 字面量 vs new Object():对于简单对象,使用字面量{}在性能和简洁性上都优于new Object()。JS引擎对字面量有优化。
  2. 方法定义的位置:将方法放在原型上,而不是构造函数内部,是至关重要的性能优化。对于要创建成千上万个实例的场景,这能节省巨大的内存。
  3. 属性初始化:在构造函数中一次性初始化所有属性,比后续动态添加属性要快。JS引擎可以对对象的结构进行优化(隐藏类优化)。
  4. 避免频繁改变原型:在运行时频繁地给原型添加/删除方法,会干扰JS引擎的优化,可能导致性能回退。尽量在定义类型时就确定好原型结构。

5.3 如何选择适合的模式?

场景推荐模式理由
创建少量、结构简单的配置对象对象字面量简单直接,无需抽象。
需要批量创建结构类似,但无需类型识别和共享方法的对象工厂模式封装创建逻辑,代码简洁。
需要创建具有明确类型、可识别且需要共享方法的复杂对象 (ES5环境)组合构造函数与原型模式内存高效、实例独立、类型清晰,是黄金标准。
现代开发环境 (ES6+)Class 语法语法简洁、意图明确,是组合模式的官方语法糖,首选。
需要为内置类型添加特殊功能且避免污染原型寄生构造函数模式(慎用)一种变通方案,但通常有更好的替代。
极高安全要求的封闭环境稳妥构造函数模式数据完全私有,但牺牲了便利性和原型链。

对于绝大多数现代JavaScript开发,我的建议非常明确:直接使用 ES6 的class。它清晰、标准、且被所有现代框架和库广泛支持。理解其背后的原型机制,是为了让你在遇到诡异Bug时能深入排查,而不是为了让你回头去写ES5风格的代码。

对象创建的优化过程,本质上是我们对代码可维护性、内存效率和语义清晰度不断追求的过程。从散乱的字面量到组织严密的类,每一步进化都为了解决一个具体的痛点。当你下次再敲下class关键字时,希望你能会心一笑,知道这简洁语法的背后,是JavaScript对象系统多年演进的智慧结晶。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询