
1. 对象创建从“百炼成仙”到“原型真相”在JavaScript的世界里对象是构建一切的基石。无论是处理一个简单的用户配置还是构建一个复杂的单页应用你几乎无时无刻不在和对象打交道。很多开发者尤其是刚入门的同学可能最熟悉的就是用一对花括号{}来创建对象这确实是最直接的方式。但随着项目复杂度的提升或者当你开始阅读一些开源库的源码时你会发现创建对象的方式远不止一种。不同的创建方式背后是截然不同的设计思想、内存管理机制和继承模式。理解这些就像是掌握了从“JavaScript初步”到“JavaScript高手”的必经心法。今天我们就来系统性地拆解JavaScript中创建对象的几种核心方式。这不仅仅是回答“有哪些”的问题更重要的是理解“为什么”要这样用以及在不同场景下“如何选择”。我们会从最基础的“字面量”和“构造函数”开始逐步深入到“工厂模式”、“原型模式”最后探讨现代JavaScript中更优雅的“类”Class语法。过程中我会穿插一些实际开发中踩过的“坑”比如闭包导致的内存问题、原型链污染的风险以及如何利用这些知识去解析那些令人头疼的“隐式转换”场景比如为什么[] ![]会是true。无论你是想夯实基础还是希望写出更健壮、更高效的代码这篇梳理都会对你有所帮助。2. 基础构建字面量与构造函数的朴素之道当我们谈论创建对象时最直观、最常用的两种方式就是对象字面量和new构造函数。它们是JavaScript对象世界的起点看似简单却蕴含着语言设计的基本哲学。2.1 对象字面量简洁明了的声明式语法对象字面量大概是每个JS开发者写的第一个对象。它的语法极其简单用花括号{}包裹一组键值对。const user { name: 小明, age: 25, greet() { console.log(你好我是${this.name}); } };为什么首选字面量简洁性与可读性代码一目了然属性、方法及其初始值都集中在一处声明对于创建简单的、一次性的数据结构如配置对象、数据传输对象DTO是首选。性能考量在现代JavaScript引擎中对象字面量的解析和创建通常经过高度优化速度很快。JSON兼容对象字面量的语法是JSON的超集JSON不允许函数和undefined作为值这使得它与网络数据交换格式JSON无缝对接。实操心得与注意事项动态属性名ES6之后你可以在字面量中使用计算属性名这在需要根据变量创建属性时非常有用。const propKey userId; const dynamicObj { [propKey]: 12345, [${propKey}Hash]: abcde };方法简写如上例中的greet()是ES6的方法简写语法比greet: function() {...}更简洁。原型指向通过字面量创建的对象其原型是Object.prototype。这意味着它自动拥有toString、hasOwnProperty等方法。注意虽然字面量很强大但它不适合需要创建多个具有相同结构对象的场景。重复编写相似的字面量会导致代码冗余且修改公共结构时需要在多处同步容易出错。2.2 new 构造函数系统内置的构建蓝图JavaScript为一些基础类型提供了内置的构造函数如Object、Array、Date、RegExp等。使用new关键字调用它们可以创建对应的对象实例。const obj new Object(); // 等价于 const obj {};但后者更简洁 const arr new Array(1, 2, 3); // 创建数组 [1, 2, 3] const date new Date(); // 创建当前时间的Date对象 const regex new RegExp(\\d, g); // 创建正则表达式对象为什么有时会用new动态构建当构造函数的参数需要动态计算时new Constructor(arg)的写法比字面量更自然。历史与明确性在早期代码或某些特定API中较为常见。使用new Array(length)可以创建指定长度的空数组但要注意这带来的“空位”问题。踩过的坑new Array(数字)的陷阱new Array(3)创建的是一个长度为3的空位数组[empty × 3]而不是[undefined, undefined, undefined]。在map、forEach等方法中空位会被跳过这常常导致意料之外的行为。因此在实践中更推荐使用字面量[]或Array.from({length: 3})。基本类型包装对象new String(‘hello’)、new Number(42)创建的是对象而不是基本类型。这会导致typeof判断和比较运算出现问题绝对不要这样使用基本类型直接使用字面量即可。忘记new关键字对于某些构造函数尤其是自定义的如果调用时漏写了new在非严格模式下this会指向全局对象如window造成属性泄露和难以调试的bug。这是使用构造函数模式的一个经典风险点。小结对于简单、单一的对象字面量是王道。对于内置的复杂类型如Date,RegExpnew构造函数是标准用法。但务必警惕其中的陷阱。3. 模式演进从工厂函数到原型链的抽象之旅当我们需要批量创建具有相同接口和行为的对象时基础方法就显得力不从心了。这时我们需要借助一些设计模式来提升代码的复用性和可维护性。工厂模式和原型模式是两种经典解决方案。3.1 工厂模式封装创建过程的函数工厂模式的核心思想是用一个函数来封装创建对象的细节包括使用字面量或new等并返回这个新对象。调用者无需关心对象的具体构建过程。function createUser(name, age) { // 在函数内部使用对象字面量创建并返回 return { name, age, greet() { console.log(我是${this.name}); } }; } const user1 createUser(小红, 22); const user2 createUser(小刚, 30);为什么选择工厂模式解耦将对象的创建逻辑集中在一个地方如果创建逻辑需要修改例如需要增加一个默认属性只需改动工厂函数即可。解决代码冗余避免了为每个相似对象重复编写字面量代码。灵活性可以在函数内部根据参数进行条件判断创建不同“子类”的对象。实操心得无法进行实例类型识别这是工厂模式最大的缺点。user1 instanceof createUser会返回false因为instanceof运算符检查的是原型链而工厂函数返回的都是普通的Object实例。这使得在调试或需要类型判断时不太方便。内存效率问题如上例所示每个对象都拥有自己独立的greet方法。创建100个对象就会在内存中存在100个功能完全相同的greet函数副本这显然是一种浪费。3.2 构造函数模式定义对象“类型”为了解决工厂模式的类型识别问题JavaScript提供了构造函数模式。构造函数本质上就是一个普通函数但通常约定函数名首字母大写并且必须使用new操作符调用。function User(name, age) { // this 指向新创建的对象实例 this.name name; this.age age; this.greet function() { console.log(我是${this.name}); }; } const user1 new User(小红, 22); const user2 new User(小刚, 30); console.log(user1 instanceof User); // true解决了类型识别问题 console.log(user1.greet user2.greet); // false方法依然不共享new操作符背后发生了什么这是一个关键原理。当执行new User(...)时引擎做了四件事创建创建一个全新的空对象。链接将这个新对象的内部[[Prototype]]即__proto__链接到构造函数的prototype属性指向的对象。绑定将构造函数内部的this绑定到这个新对象。返回如果构造函数没有显式返回一个对象则自动返回这个新创建的对象。依然存在的内存问题构造函数模式虽然解决了类型识别但每个实例的方法仍然是独立的。user1.greet和user2.greet指向不同的函数对象内存浪费的问题依旧存在。3.3 原型模式共享属性和方法的终极蓝图原型模式是JavaScript实现继承和共享的核心机制。每个函数都有一个prototype原型属性它是一个对象。当通过new调用构造函数创建实例时该实例的内部原型指针会指向构造函数的prototype对象。核心思想将需要共享的属性和方法定义在构造函数的prototype对象上。这样所有实例都能访问到这些共享成员但它们只在内存中存在一份。function User(name, age) { // 实例属性每个对象独有 this.name name; this.age age; } // 共享方法定义在原型上 User.prototype.greet function() { console.log(我是${this.name}); }; // 甚至可以定义共享属性通常用于常量或默认值需谨慎 User.prototype.species ‘人类’; const user1 new User(‘小红’, 22); const user2 new User(‘小刚’, 30); console.log(user1.greet user2.greet); // true方法现在是共享的 user1.greet(); // “我是小红” console.log(user1.species); // “人类”原型链查找机制当访问user1.greet时引擎首先在user1实例自身查找greet属性没找到。然后它会沿着user1.__proto__即User.prototype向上查找在这里找到了于是调用它。这就是原型链。为什么这是更优的方案极致的内存效率共享方法只需定义一次无论创建多少实例。动态性即使在实例创建之后修改User.prototype所有已存在的和将来创建的实例都能立即“看到”这个变化因为查找是动态的。User.prototype.sayBye function() { console.log(‘再见’); }; user1.sayBye(); // 可以立即调用注意事项与深坑共享引用类型属性的陷阱千万不要在prototype上定义引用类型的共享属性如数组、对象。function BadUser() {} BadUser.prototype.friends []; // 危险 const u1 new BadUser(); const u2 new BadUser(); u1.friends.push(‘小明’); console.log(u2.friends); // [‘小明’]u2的friends也被修改了这是因为所有实例访问的都是同一个数组对象。这种非预期的数据共享是原型模式最常见的坑。实例特有的数据一定要在构造函数内部用this.xxx来定义。重写原型对象如果直接给Constructor.prototype赋一个新的对象会切断已存在实例与新的原型对象之间的联系。function User() {} const u1 new User(); User.prototype { greet: function() {} }; // 重写原型 const u2 new User(); console.log(u1 instanceof User); // false! (取决于实现但关系已混乱) console.log(u2 instanceof User); // true最佳实践是永远通过Constructor.prototype.methodName ...的形式来扩展原型而不是整体替换。constructor属性默认情况下prototype对象有一个constructor属性指回构造函数本身。重写原型对象会丢失这个属性可能影响某些库的功能。如果需要可以手动设置NewPrototype.constructor Constructor。原型模式是理解JavaScript面向对象编程的基石。它优雅地解决了方法和共享属性的存储问题但其反直觉的特性也需要我们格外小心。4. 组合与进化组合模式与Class语法糖在实际项目中纯粹的构造函数模式或纯粹的原型模式都很少单独使用。更常见的做法是组合两者优点并最终演化为现代ES6的Class语法。4.1 组合构造函数与原型模式最广泛使用的经典模式这是ES5时代创建自定义“类”的事实标准。其规则非常简单实例属性在构造函数内部使用this.xxx ...定义。每个实例都有自己的一份副本。共享方法在构造函数的prototype对象上定义。所有实例共享一份。// 1. 定义构造函数初始化实例属性 function Person(name, age) { this.name name; this.age age; this.hobbies []; // 引用类型每个实例独立 } // 2. 在原型上定义共享方法 Person.prototype.greet function() { console.log(你好我是${this.name}今年${this.age}岁。); }; Person.prototype.addHobby function(hobby) { this.hobbies.push(hobby); // 操作的是实例自身的hobbies数组 }; // 使用 const p1 new Person(‘张三’, 20); const p2 new Person(‘李四’, 25); p1.addHobby(‘读书’); p2.addHobby(‘游泳’); console.log(p1.hobbies); // [‘读书’] console.log(p2.hobbies); // [‘游泳’] 互不影响 console.log(p1.greet p2.greet); // true 方法共享这个模式好在哪里它完美地规避了之前提到的所有陷阱实例拥有独立的属性特别是引用类型数据安全。方法在原型上共享内存高效。可以通过instanceof正确识别类型。代码结构清晰职责分离。在ES6之前几乎所有的JavaScript库和框架在定义“类”时都采用这种模式。它是理解后续所有高级模式的基础。4.2 动态原型模式更优雅的代码组织组合模式有一个小缺点定义方法的代码Person.prototype.xxx ...和定义构造函数的代码是分离的。动态原型模式旨在将两者封装在同一个函数块中使代码更聚合。function Person(name, age) { this.name name; this.age age; this.hobbies []; // 检查原型上的某个方法是否已定义避免重复初始化 if (typeof Person.prototype.greet ! ‘function’) { Person.prototype.greet function() { console.log(你好我是${this.name}); }; Person.prototype.addHobby function(hobby) { this.hobbies.push(hobby); }; // ... 可以继续添加其他共享方法 } }原理利用new调用时原型对象已经存在且与构造函数关联的特性。通过判断原型上某个标志性方法是否存在来决定是否初始化原型方法。这样无论调用多少次new Person()原型方法的赋值操作只执行一次。个人体会动态原型模式在封装性上更胜一筹但对于初学者来说理解其“只初始化一次”的逻辑需要多花点心思。在小型项目或简单的类定义中传统的组合模式分离定义因其直观性可能更受欢迎。但在构建大型框架或库需要强封装时动态原型模式是更好的选择。4.3 ES6 Class语法糖与现代化标准ES6引入的class关键字并没有向JavaScript引入新的面向对象继承模型它只是上述组合构造函数与原型模式的语法糖让代码写法更清晰、更像传统面向对象语言。class Person { // 构造函数对应之前的 function Person(...) constructor(name, age) { this.name name; // 实例属性 this.age age; this.hobbies []; } // 类方法会自动被添加到 Person.prototype 上 greet() { console.log(你好我是${this.name}); } addHobby(hobby) { this.hobbies.push(hobby); } // 静态方法属于类本身而不是实例。通过 Person.staticMethod() 调用 static describe() { console.log(‘这是一个描述人的类’); } } // 使用方式完全一致 const p1 new Person(‘王五’, 28); p1.greet(); // 调用原型方法 Person.describe(); // 调用静态方法 console.log(typeof Person); // “function”类本质仍是函数 console.log(p1 instanceof Person); // true console.log(p1.greet Person.prototype.greet); // trueClass 语法带来的核心优势语法更简洁直观将构造器、方法、静态方法、getter/setter、继承等集中在一个块级作用域内声明结构清晰。内置“严格模式”类声明和类表达式中的代码默认在严格模式下执行有助于避免一些错误。更安全的继承使用extends和super实现继承比ES5中手动修改原型链Child.prototype new Parent()更安全、更易理解并且能正确调用父类构造函数。清晰的静态成员定义使用static关键字定义静态方法或属性意图明确。注意事项类声明不会提升与函数声明不同类声明不存在提升。必须在定义之后才能使用。类方法不可枚举在class中定义的方法其enumerable属性默认为false这意味着在for...in循环中不会被遍历到而直接在prototype上添加的方法默认是可枚举的。这通常更符合预期。必须使用new调用类构造函数必须使用new调用否则会抛出错误。这避免了普通构造函数忘记写new导致的this指向全局的bug。Class 是未来。对于新的项目除非有特殊的兼容性要求需要支持非常古老的浏览器否则都应该使用class语法来定义对象蓝图。它更现代更不易出错社区接受度也最高。5. 高级模式与特殊场景应对复杂需求除了上述主流模式还有一些特定场景下非常有用的对象创建方式。5.1 Object.create()以指定原型创建纯净对象Object.create()方法创建一个新对象并使用现有的对象作为新创建对象的原型。这是最直接、最纯净的基于原型创建对象的方式。const personPrototype { greet() { console.log(你好我是${this.name}); } }; // 创建一个以 personPrototype 为原型的新对象 const person1 Object.create(personPrototype); person1.name ‘小明’; // 添加实例属性 person1.greet(); // “你好我是小明” console.log(Object.getPrototypeOf(person1) personPrototype); // true应用场景与优势创建没有原型的对象Object.create(null)可以创建一个真正的空对象它连Object.prototype都没有因此不会有toString、hasOwnProperty等方法。这在需要创建一个纯净的字典键值对集合以避免属性名冲突时非常有用例如用作Map的替代品。实现纯净的继承在ES5时代这是实现原型式继承的核心方法。可以更灵活地控制原型链而不依赖于构造函数。与new无关它完全避开了构造函数和new关键字是一种更函数式的对象创建方式。5.2 寄生构造函数模式与稳妥构造函数模式这两种是相对小众的模式用于解决一些特殊问题。寄生构造函数模式它看起来像一个构造函数用new调用但内部实际上是通过其他方式如返回一个包装过的已有对象来创建并返回对象。常用于扩展一个已有类型但又不想直接修改其原型。function SpecialArray() { // 创建一个数组 const arr new Array(); // 添加额外属性/方法 arr.push.apply(arr, arguments); arr.toPipedString function() { return this.join(‘|’); }; // 返回这个加工后的数组 return arr; } const colors new SpecialArray(‘red’, ‘blue’, ‘green’); console.log(colors.toPipedString()); // “red|blue|green” console.log(colors instanceof SpecialArray); // false它其实是Array的实例注意由于返回的对象与构造函数SpecialArray的原型没有关系instanceof操作符会失效。这种模式主要用于特殊场景日常开发中应谨慎使用。稳妥构造函数模式由道格拉斯·克罗克福德提出旨在创建“安全”的对象其特点是不引用this也不使用new操作符所有数据都通过闭包来保持私有只暴露必要的公共API。这类似于工厂模式但更强调数据封装。function SecurePerson(name, age) { const privateData { secret: ‘mySecret’ }; // 私有数据 const o {}; // 创建一个新对象 // 定义特权方法可以访问私有数据 o.getPublicInfo function() { return Name: ${name}, Age: ${age}; // 可以访问传入的参数 }; o.getSecret function() { return privateData.secret; }; return o; // 返回这个安全对象 } const person SecurePerson(‘Alice’, 30); // 注意没有 new console.log(person.getPublicInfo()); // “Name: Alice, Age: 30” console.log(person.name); // undefined无法直接访问 console.log(person.getSecret()); // “mySecret”这种模式非常适合需要高度数据安全性的环境但代价是每个实例的方法都不是共享的会消耗更多内存。6. 实战避坑与性能考量了解了各种创建方式后如何在项目中做选择以下是一些实战经验和性能上的考量。6.1 模式选择速查表场景推荐模式关键理由创建简单的配置对象、一次性数据结构对象字面量{}简洁、直观、性能好创建内置类型实例Date, RegExp等new 构造函数标准用法功能完整创建少量、结构简单的相似对象工厂模式代码集中避免冗余定义自定义类型需要实例识别和内存效率组合构造函数与原型模式或ES6 Class兼具类型识别、内存效率和清晰结构。Class是首选需要极致的封装和数据私有性稳妥构造函数模式数据完全私有API可控需要创建一个纯净的字典或指定特殊原型Object.create()灵活控制原型可创建无原型对象扩展内置类型而不污染其原型寄生构造函数模式临时性增强功能个人建议对于现代前端开发ES6 Class已经成为定义对象蓝图的标准方式应作为首选。对于简单的数据容器使用对象字面量。只有在遇到非常特殊的场景如创建Object.create(null)时才考虑其他模式。6.2 性能与内存的微观洞察虽然现代JS引擎优化得很好但模式选择仍对性能有细微影响。字面量 vs new Object()字面量{}在解析和创建速度上几乎总是优于new Object()因为引擎可以对字面量进行静态分析和优化。原型方法 vs 实例方法将方法定义在原型上无论在内存占用还是创建速度上都远优于在构造函数内部用this.method function(){}为每个实例创建方法。这是最重要的性能优化点之一。Class 的性能class语法经过引擎高度优化其性能与经典的“组合构造函数与原型模式”基本一致有时甚至更优。无需担心语法糖带来的性能损耗。闭包与内存稳妥构造函数模式或工厂模式中如果方法使用了构造函数内的变量形成闭包这些变量无法被垃圾回收即使实例本身不再使用。在需要创建大量实例时需警惕潜在的内存泄漏。6.3 常见问题排查实录问题1为什么修改了原型对象但某个实例访问不到新方法排查检查是否在实例创建之后才重写替换了整个原型对象。如前所述Constructor.prototype { ... }会切断旧实例与新旧原型之间的联系。已存在的实例仍然指向旧的原型对象。解决始终通过Constructor.prototype.newMethod ...的方式增量添加方法。问题2instanceof判断失灵排查是否使用了工厂模式返回普通对象是否在寄生构造函数模式中返回了其他类型的对象是否跨了iframe或不同的全局环境不同环境的相同构造函数引用不相等。解决确保使用正确的模式。在需要类型判断的复杂场景可以考虑使用Object.getPrototypeOf()或obj.constructor需注意constructor属性可能被重写进行更精确的判断。问题3对象属性被意外共享排查这是原型模式最经典的坑。请检查是否将引用类型数组、对象定义在了构造函数的prototype上。function Problem() {} Problem.prototype.data []; // 危险解决将需要实例独享的属性特别是引用类型定义在构造函数内部function Problem() { this.data []; }。问题4使用Class时如何定义真正的“私有字段”排查在ES6 Class中直接声明在类体内的属性仍是公开的。虽然社区有使用_前缀表示私有的约定但这只是约定并非语言强制。解决ES2022使用#前缀定义真正的私有字段。class MyClass { #privateField ‘secret’; getPrivate() { return this.#privateField; } }ES6环境利用WeakMap或闭包在构造函数内定义变量和访问函数来模拟私有性但这会增加代码复杂度。理解对象创建的方式不仅仅是记住几种语法更是深入理解JavaScript原型继承这一核心语言特性的过程。从简单的{}到复杂的Class每一种模式都是为解决特定问题而演化出的方案。在实际开发中我强烈建议将ES6 Class作为创建对象蓝图的标准工具它清晰、安全且现代。同时把对象字面量用于数据聚合把Object.create()留给需要精细控制原型的特殊场景。时刻警惕原型上共享引用类型的陷阱并在性能敏感的场景下有意识地将方法定义在原型上。掌握了这些你就能在JavaScript的对象世界里游刃有余无论是阅读框架源码还是设计自己的模块都会更加得心应手。