
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); } };优点极其简单明了一眼就能看懂对象的结构。致命缺点代码冗余每个属性都要重复写一遍sayName方法在每个对象里都重新定义了一次。毫无辨识度我们无法一眼看出person1和person2是由同一个“模子”创建的它们之间没有内在联系。注意在只有两三个对象时这种方式无可厚非。但一旦数量增多或结构变复杂它就会立刻成为维护的噩梦。记住一个原则重复的代码是“代码坏味道”的第一信号。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, ‘设计师‘);优点封装了创建过程我们不用再关心对象内部如何构建只需传入参数。解决了代码重复对象的创建逻辑只有一份。依然存在的问题对象类型识别问题person1和person2通过instanceof检查时都指向Object而不是我们期望的Person。我们无法知道它们是由哪个“工厂”创建的。方法重复定义每个对象仍然拥有自己独立的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这里发生了四件关键事情在内存中创建一个新对象。这个新对象内部的[[Prototype]]即__proto__特性被赋值为构造函数的prototype属性。构造函数内部的this被赋值为这个新对象。执行构造函数内部的代码给新对象添加属性。如果构造函数没有显式返回其他对象则自动返回这个新对象。划时代的优点类型识别instanceof和constructor属性都能正确标识对象的“出身”解决了工厂模式的最大痛点。约定俗成构造函数名首字母大写这是一种强烈的视觉信号提醒开发者此函数需要用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引擎会执行以下搜索首先在对象实例本身查找是否有该属性。如果没有则沿着[[Prototype]]指针去它的原型对象上查找。如果原型对象上还没有就继续去原型的原型上找直到找到Object.prototype所有对象的顶层原型为止。这就是原型链。优点极致的内存效率无论创建多少实例共享方法在内存中只存在一份。动态性即使实例已经创建我们仍然可以通过修改Person.prototype来为所有实例添加新的共享方法。缺点与注意事项共享引用类型属性的陷阱这是原型模式最大的坑。如果原型属性是一个引用类型如数组、对象那么所有实例将共享同一个引用。function Person() {} Person.prototype.friends [‘小王‘, ‘小李‘]; // 引用类型属性 const p1 new Person(); const p2 new Person(); p1.friends.push(‘小张‘); console.log(p2.friends); // [‘小王‘, ‘小李‘, ‘小张‘]p2的friends也被修改了避坑指南永远不要将需要独立维护的引用类型属性放在原型上。实例特有的属性务必在构造函数内部用this.xxx来定义。无法通过实例重写原型person1.prototype是undefined。实例只能访问原型不能直接修改构造函数的原型引用。参数传递问题纯原型模式无法在创建实例时差异化地初始化属性。因此它通常不单独使用。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 方法共享为什么这是“终极”模式内存高效方法只创建一次供所有实例使用。实例独立每个实例拥有自己的属性副本特别是引用类型属性互不干扰。类型识别清晰instanceof和constructor都能正确工作。参数化创建可以通过构造函数传递参数灵活初始化每个实例。这个模式是如此经典和有效以至于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 稳妥构造函数模式这是一种强调安全性的模式适用于某些安全环境如防止数据被第三方脚本篡改它不依赖this和new。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); // trueClass 语法带来的核心便利统一的书写结构将构造逻辑和原型方法定义放在一个class块中结构更清晰。更直观的继承通过extends和super实现继承比ES5的原型链继承写法直观太多。内置的严格模式类声明和类表达式中的代码默认在严格模式下执行。静态方法和访问器属性语法支持更完善。不可枚举类中定义的方法默认是不可枚举的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 对象创建的性能考量字面量 vs new Object()对于简单对象使用字面量{}在性能和简洁性上都优于new Object()。JS引擎对字面量有优化。方法定义的位置将方法放在原型上而不是构造函数内部是至关重要的性能优化。对于要创建成千上万个实例的场景这能节省巨大的内存。属性初始化在构造函数中一次性初始化所有属性比后续动态添加属性要快。JS引擎可以对对象的结构进行优化隐藏类优化。避免频繁改变原型在运行时频繁地给原型添加/删除方法会干扰JS引擎的优化可能导致性能回退。尽量在定义类型时就确定好原型结构。5.3 如何选择适合的模式场景推荐模式理由创建少量、结构简单的配置对象对象字面量简单直接无需抽象。需要批量创建结构类似但无需类型识别和共享方法的对象工厂模式封装创建逻辑代码简洁。需要创建具有明确类型、可识别且需要共享方法的复杂对象 (ES5环境)组合构造函数与原型模式内存高效、实例独立、类型清晰是黄金标准。现代开发环境 (ES6)Class 语法语法简洁、意图明确是组合模式的官方语法糖首选。需要为内置类型添加特殊功能且避免污染原型寄生构造函数模式(慎用)一种变通方案但通常有更好的替代。极高安全要求的封闭环境稳妥构造函数模式数据完全私有但牺牲了便利性和原型链。对于绝大多数现代JavaScript开发我的建议非常明确直接使用 ES6 的class。它清晰、标准、且被所有现代框架和库广泛支持。理解其背后的原型机制是为了让你在遇到诡异Bug时能深入排查而不是为了让你回头去写ES5风格的代码。对象创建的优化过程本质上是我们对代码可维护性、内存效率和语义清晰度不断追求的过程。从散乱的字面量到组织严密的类每一步进化都为了解决一个具体的痛点。当你下次再敲下class关键字时希望你能会心一笑知道这简洁语法的背后是JavaScript对象系统多年演进的智慧结晶。