恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
JavaScript对象创建全解析:7种方式与最佳实践
首页
资讯中心
/
JavaScript对象创建全解析:7种方式与最佳实践
JavaScript对象创建全解析:7种方式与最佳实践
发布时间:2026/8/28 3:16:00
1. 从“new Object()”说起为什么我们需要这么多创建方式在JavaScript的世界里对象是构建一切的基石。无论是前端页面上的一个按钮组件还是后端Node.js中的一个用户模型本质上都是一个对象。很多开发者尤其是刚入门的同学最熟悉的可能就是new Object()或者直接用字面量{}来创建对象。这当然没错但如果你只停留在这两种方式就像只学会了用螺丝刀拧螺丝却不知道还有扳手、钳子、电钻一样面对复杂的工程问题时可能会事倍功半。我见过不少项目因为对象创建方式单一导致代码冗余、难以维护或者性能上存在不必要的开销。比如需要创建上百个结构相似的对象时还在用字面量一个个写或者需要实现类似“类”的继承机制时用各种奇技淫巧去拼凑原型链既容易出错又让后来者难以理解。实际上JavaScript提供了至少7种各具特色的对象创建方式。每一种方式都不是多余的它们诞生于语言发展的不同阶段为了解决特定场景下的问题。理解它们不仅仅是记住语法更是理解JavaScript这门语言“基于原型”的核心思想以及如何根据不同的需求选择最优雅、最高效的“工具”。今天我们就来彻底拆解这7种方式从最基础的到最现代的我会结合我多年踩坑的经验告诉你每种方式“为什么”存在以及“什么时候”该用它。2. 基础构建字面量与构造函数这是最直观、最常用的两种方式几乎每个JavaScript程序都离不开它们。但即使是最基础的东西里面也有不少值得深究的细节和容易踩的坑。2.1 对象字面量简洁明了的起点对象字面量大概是所有方式里最讨人喜欢的一个了。它的语法极其简单const person { name: 小明, age: 25, greet() { console.log(你好我是${this.name}); } };为什么这种方式如此流行首先它声明即定义在代码中一目了然对象的全部结构和初始值都摆在那里可读性极高。其次它在现代JavaScript引擎中经过了极度优化性能通常是最好的。因为引擎在解析阶段就能完全确定对象的结构shape可以为其分配最优的内存布局甚至进行内联缓存等优化。但是这里有一个新手极易忽略的“坑”字面量创建的对象其原型指向Object.prototype。这意味着你创建的对象自动拥有了toString、valueOf、hasOwnProperty等方法。这通常是好事但如果你需要创建一个完全纯净、没有任何原型的对象例如要作为一个纯粹的字典来存储键值对避免原型上的属性被意外遍历或覆盖字面量就不行了。实操心得在绝大多数情况下当你需要一个结构固定、一次性使用的“数据载体”或“配置对象”时请毫不犹豫地使用字面量。例如函数的参数选项、API请求的载荷、组件的属性集合等。它的简洁和高效是无与伦比的。2.2 Object构造函数略显古老但仍有其用这是另一种基础方式通过new关键字调用Object构造函数const person new Object(); person.name 小明; person.age 25; person.greet function() { console.log(你好我是${this.name}); };或者你也可以给构造函数传入一个值它会根据参数类型返回对应的包装对象或空对象const obj new Object(); // 等同于 {} const strObj new Object(hello); // 等同于 new String(hello) const numObj new Object(123); // 等同于 new Number(123)那么既然有了字面量为什么还需要new Object()呢在当今的编码实践中直接使用new Object()的情况确实很少了因为字面量{}在几乎所有方面都更胜一筹更简洁、更易读、性能可能更好。但是理解它有助于理解JavaScript的底层机制。Object本身是一个函数new Object()这个操作揭示了JavaScript中构造函数创建对象的基本模式。一个重要的细节是Object()即使不带new调用行为也很特殊。Object()会执行类型转换而new Object()总是创建一个新对象。但Object()本身作为函数如果传入null或undefined它会返回一个空对象这有时可以用于安全地转换值。什么时候会用到它坦白说在现代开发中直接写new Object()的场景极少。但在一些底层工具库、或者需要与非常古老的代码库保持一致的场景下你可能会遇到它。更常见的用法是使用Object上的静态方法例如Object.create()、Object.assign()等这些我们后面会详细讲。避坑提示除非有特别理由否则在新代码中请优先使用对象字面量{}。使用new Object()可能会让其他开发者觉得你的代码风格有些过时。3. 工厂模式封装创建细节当我们遇到需要批量创建结构相似的对象时重复写对象字面量就显得非常冗余和低效。这时工厂模式就派上用场了。它的核心思想是用一个函数来封装创建对象的细节这个函数就像一个“工厂”你传入原料参数它返回一个制作好的“产品”对象。function createPerson(name, age) { const obj {}; // 或者 new Object() obj.name name; obj.age age; obj.greet function() { console.log(你好我是${this.name}); }; return obj; } const xiaoming createPerson(小明, 25); const xiaohong createPerson(小红, 23);为什么需要工厂模式减少重复代码这是最直接的好处。对象的创建逻辑被集中在一处修改时只需改一个地方。隐藏实现细节调用者比如xiaoming不需要关心对象内部是如何构建的只需要调用工厂函数并传入必要的参数。可以实现更复杂的初始化逻辑工厂函数内部可以进行参数校验、设置默认值、甚至根据条件创建不同类型的对象。但是工厂模式有一个显著的缺点内存效率问题。注意看上面的代码每一个通过createPerson创建的对象都拥有自己独立的greet方法。如果创建一千个person对象就会在内存中存在一千个功能完全相同的greet函数副本这无疑是一种浪费。如何优化一个常见的改进是将方法定义在工厂函数外部然后在内部通过引用赋值function greet() { console.log(你好我是${this.name}); } function createPerson(name, age) { const obj {}; obj.name name; obj.age age; obj.greet greet; // 引用同一个函数 return obj; }这样所有对象共享同一个greet函数节省了内存。但这又带来了新的问题这个greet函数成了全局函数污染了命名空间而且对象和方法之间的关系变得松散不那么“面向对象”了。工厂模式的适用场景当你需要创建一系列结构类似、但不需要复杂继承关系的对象并且对内存开销不是极度敏感时工厂模式是一个清晰且实用的选择。它特别适合创建一些配置对象、数据模型实例或者简单的UI组件。我的经验是在小型应用、工具函数或者脚本中工厂模式简单直接。但在大型、复杂的面向对象应用中我们通常需要更强大的机制来管理对象之间的关系尤其是继承这就引出了我们接下来要讨论的——构造函数模式。4. 构造函数模式模拟“类”的尝试为了解决工厂模式中方法无法共享、对象类型标识模糊的问题JavaScript引入了构造函数模式。这是ES5及之前时代模拟传统面向对象语言中“类”概念的主要方式。function Person(name, age) { // 构造函数内部的 this 指向新创建的对象实例 this.name name; this.age age; this.greet function() { console.log(你好我是${this.name}); }; // 注意构造函数通常不显式返回return一个对象。 // 如果返回原始值会被忽略如果返回对象则会覆盖new创建的对象。 } // 使用 new 关键字调用 const xiaoming new Person(小明, 25); const xiaohong new Person(小红, 23); console.log(xiaoming instanceof Person); // true console.log(xiaoming.constructor Person); // true关键机制解析new操作符的作用当使用new调用函数时会发生四件事创建一个全新的空对象。将这个新对象的内部[[Prototype]]即__proto__链接到构造函数的prototype属性指向的对象。将构造函数内部的this绑定到这个新对象。执行构造函数内部的代码通常是为this添加属性。如果构造函数没有返回其他对象则自动返回这个新创建的对象。instanceof操作符它用于检测构造函数的prototype属性是否出现在某个对象的原型链上。这为对象提供了明确的类型标识。构造函数模式解决了工厂模式的两个问题类型标识xiaoming instanceof Person为true我们可以明确知道它是由Person创建的。方法共享初步解决但注意上面代码中的greet方法仍然是在每个实例上重新创建的内存问题依旧存在。真正的共享将方法挂载到原型上构造函数的精髓在于其prototype属性。所有由该构造函数创建的对象实例都会共享其prototype对象上的属性和方法。function Person(name, age) { // 实例属性每个对象独有 this.name name; this.age age; } // 共享方法定义在原型上 Person.prototype.greet function() { console.log(你好我是${this.name}); }; Person.prototype.species 人类; // 共享属性 const xiaoming new Person(小明, 25); const xiaohong new Person(小红, 23); console.log(xiaoming.greet xiaohong.greet); // true是同一个函数现在无论创建多少个Person实例greet方法在内存中只有一份极大地节省了资源。这是构造函数模式相比工厂模式的核心优势。踩坑实录忘记使用new关键字这是使用构造函数模式时最常见的错误const xiaowang Person(小王, 28); // 忘记写 new console.log(xiaowang); // undefined console.log(window.name); // 小王 在浏览器非严格模式下this指向了全局window在没有new的情况下调用构造函数this在非严格模式下会指向全局对象如window导致属性被意外添加到全局函数也没有返回对象所以xiaowang是undefined。如何避免有两个常见做法命名约定构造函数首字母大写如Person提醒调用者必须使用new。安全构造函数在构造函数内部判断this是否是当前构造函数的实例。function Person(name, age) { // 如果 this 不是 Person 的实例说明可能没被 new 调用 if (!(this instanceof Person)) { return new Person(name, age); // 自动补上 new } this.name name; this.age age; } // ES6 中可以使用 new.target 进行更优雅的判断构造函数模式是理解JavaScript原型继承的基石。它虽然比工厂模式复杂但提供了更好的代码组织、内存效率和类型系统。然而它的语法对于习惯了经典类继承的开发者来说仍然显得有些怪异和繁琐。于是ECMAScript委员会在ES5中提供了一个更优雅的原型操作工具——Object.create()。5. Object.create纯粹的原型继承如果说构造函数模式是“模拟类”那么Object.create()就是“拥抱原型”。它于ES5中被标准化提供了一种极其简洁、直接的方式来创建一个新对象并将其原型明确指向另一个对象。const personPrototype { greet() { console.log(你好我是${this.name}); }, species: 人类 }; const xiaoming Object.create(personPrototype); xiaoming.name 小明; // 设置自身的属性 xiaoming.age 25; xiaoming.greet(); // “你好我是小明” console.log(xiaoming.species); // “人类” console.log(Object.getPrototypeOf(xiaoming) personPrototype); // true它的工作原理非常直观Object.create(proto)创建一个全新的空对象并将这个空对象的[[Prototype]]内部属性设置为传入的proto对象。你可以选择性地传入第二个参数用于定义新对象自身的属性描述符是否可写、可枚举等。为什么说它“纯粹”因为它完全跳过了“构造函数”和“new”的概念直接操作原型链。你不需要定义一个像Person这样的构造函数只需要有一个作为蓝本的“原型对象”。创建对象时原型关系一目了然。Object.create的几种关键用法创建纯净的空对象Object.create(null)。这会创建一个没有任何原型的对象也就是[[Prototype]]为null。它非常适合用作纯粹的键值对映射字典因为不会受到Object.prototype上诸如toString、hasOwnProperty等方法的干扰在for...in循环中也不会遍历到原型上的属性性能和安全度更高。实现继承在ES5时代这是实现对象继承的推荐方式。// 父类“原型” const animal { eat() { console.log(${this.name} 在吃东西。); } }; // 子类“原型”继承自animal const dog Object.create(animal); dog.bark function() { console.log(汪汪); }; // 创建子类实例 const myDog Object.create(dog); myDog.name 阿黄; myDog.eat(); // “阿黄 在吃东西。” myDog.bark(); // “汪汪”属性描述符第二个参数让你可以精细控制属性的特性。const obj Object.create(personPrototype, { name: { value: 小明, writable: false, // 只读 enumerable: true, configurable: false }, age: { value: 25, enumerable: true } }); obj.name 小刚; // 静默失败严格模式下会报错 console.log(obj.name); // 仍然是“小明”与构造函数模式的对比语法Object.create更函数式更简洁。构造函数模式更命令式更像传统OOP。原型关系Object.create直接建立原型链关系清晰。构造函数模式的原型链需要通过new和prototype属性间接建立。初始化Object.create创建的对象自身初始是空的属性需要后续添加。构造函数可以在调用时通过参数初始化实例属性。我的选择建议当你需要精确控制对象的原型或者需要创建一个没有原型的纯净对象时Object.create是首选。在实现简单的对象组合或继承时它也显得非常优雅。然而对于需要大量实例、且每个实例都有独立初始化逻辑的“类”场景构造函数模式或ES6的Class可能更符合直觉。6. Class语法糖ES6的现代风格ES6引入的class关键字是JavaScript在语法层面的一次重大演进。它并没有引入新的面向对象继承模型其本质仍然是基于原型的可以看作是构造函数和原型继承的一种更清晰、更易读的语法糖。class Person { // 构造函数对应之前的 function Person(...) constructor(name, age) { // 实例属性 this.name name; this.age age; } // 实例方法会自动挂载到 Person.prototype 上 greet() { console.log(你好我是${this.name}); } // 静态方法属于类本身而不是实例 static compareAge(a, b) { return a.age - b.age; } // Getter 和 Setter get introduction() { return ${this.name}, ${this.age}岁; } set nickname(value) { this._nickname value; } } const xiaoming new Person(小明, 25); xiaoming.greet(); // “你好我是小明” console.log(Person.compareAge(xiaoming, new Person(小红, 23))); // 2静态方法调用 console.log(xiaoming.introduction); // “小明, 25岁”为什么说Class是语法糖因为上面这段class代码几乎完全等价于我们之前用构造函数和原型写出来的代码。constructor就是构造函数类中定义的方法就是Person.prototype上的方法。typeof Person的结果依然是function。那么Class带来了哪些实实在在的好处语法更清晰直观写法上更接近Java、C等传统语言降低了学习成本提高了代码的可读性和可维护性。继承的语法也变得极其简洁使用extends和super。内置了“严格模式”类声明和类表达式中的代码默认在严格模式下执行避免了因疏忽导致的常见错误如忘记new时this指向全局。更优雅的继承使用extends实现继承使用super调用父类构造函数或方法比ES5中手动修改原型链 (Child.prototype Object.create(Parent.prototype)) 要清晰和安全得多。支持静态方法/属性、Getter/Setter这些特性在语法层面得到了原生支持定义和使用都更加方便。私有字段ES2022正式化虽然目前还在逐步普及中但#前缀的私有字段语法为类提供了真正的私有成员能力这是过去通过闭包或Symbol模拟所不及的。Class解决了构造函数模式的哪些痛点忘记new使用Class必须通过new调用否则会直接抛出TypeError。原型方法的定义分散所有方法都定义在类体内部结构紧凑。继承实现繁琐易错extends和super让继承变得简单可靠。一个重要的注意事项Hoisting提升函数声明和类声明都会提升但方式不同。函数声明会提升整个定义包括函数体你可以在声明前调用它。而类声明虽然其标识符也会提升但在声明之前它处于“暂时性死区”访问它会抛出引用错误。new Person(); // ReferenceError: Cannot access Person before initialization class Person {}Class的适用场景对于绝大多数现代JavaScript项目尤其是需要构建复杂对象层次结构、团队协作或从其他OOP语言转过来的开发者Class是首选。它语义清晰工具链支持好如TypeScript是现代JavaScript开发的标配。然而Class并没有取代其他创建对象的方式。对于简单的数据对象、需要特殊原型如null的对象、或者在一些函数式编程风格较重的代码中对象字面量、Object.create等仍然是更合适的选择。7. 其他方式与特殊场景除了前面介绍的几种主流方式JavaScript中还有一些用于特定场景或相对小众的对象创建方法。了解它们能让你在遇到特殊需求时有更多的工具可以选择。7.1 使用Object.assign进行对象组合Object.assign()方法用于将所有可枚举的自有属性从一个或多个源对象复制到目标对象并返回目标对象。它常被用来“混合”或“组合”多个对象从而创建一个新对象。const canEat { eat() { console.log(${this.name} 在吃。); } }; const canWalk { walk() { console.log(${this.name} 在走。); } }; function createAnimal(name) { // 从一个空对象开始混合多个“行为”对象 return Object.assign({}, canEat, canWalk, { name }); } const dog createAnimal(小狗); dog.eat(); // “小狗 在吃。” dog.walk(); // “小狗 在走。”为什么用Object.assign创建对象这种方式体现了“组合优于继承”的设计思想。与其构建一个复杂的、深层次的继承链不如将小型的、功能单一的对象通常称为“mixin”混合组合起来形成更复杂的对象。这样做的好处是灵活性高耦合度低更容易复用和测试。注意点Object.assign进行的是浅拷贝。如果源对象的某个属性值是一个对象那么拷贝的是这个对象的引用而不是对象本身。const source { nested: { a: 1 } }; const target Object.assign({}, source); target.nested.a 2; console.log(source.nested.a); // 2因为 source.nested 和 target.nested 指向同一个对象7.2 使用Object.fromEntries从键值对创建ES2019引入了Object.fromEntries()方法它与Object.entries()互为逆操作。Object.entries()将对象转化为键值对数组而Object.fromEntries()则将键值对数组转化回对象。const entries [[name, 小明], [age, 25], [city, 北京]]; const person Object.fromEntries(entries); console.log(person); // { name: 小明, age: 25, city: 北京 }适用场景处理Map结构Map的entries()方法返回的就是一个迭代器可以很方便地转为对象。const map new Map([[name, 小红], [age, 23]]); const obj Object.fromEntries(map); console.log(obj); // { name: 小红, age: 23 }转换URL查询参数URLSearchParams对象也可以被转换。const params new URLSearchParams(name小明age25); const obj Object.fromEntries(params); console.log(obj); // { name: 小明, age: 25 }重构或筛选对象属性结合Object.entries()、数组的filter、map方法可以非常函数式地对对象进行转换。7.3 动态属性名与计算属性从ES6开始对象字面量支持使用方括号[]来定义计算属性名。这允许你在创建对象时动态地决定属性的键名。const propKey firstName; const index 1; const person { [propKey]: 张, // 属性名为 firstName [score${index}]: 95, // 属性名为 score1 // 也可以结合方法 [get FullName]() { return ${this.firstName}三; } }; console.log(person.firstName); // “张” console.log(person.score1); // 95 console.log(person.getFullName()); // “张三”为什么需要计算属性它极大地增加了对象字面量定义的灵活性。在处理动态数据、根据条件生成不同属性、或者实现一些元编程技巧时非常有用。在ES5中要实现同样的效果必须在创建对象后再使用方括号语法obj[动态键] 值来添加属性。计算属性让创建和定义一气呵成代码更紧凑。7.4 单例模式与模块模式严格来说这不是一种新的“创建方式”而是一种运用已有方式的设计模式。但在JavaScript中利用函数作用域和闭包可以非常优雅地实现单例一个类仅有一个实例或模块封装私有变量。// 模块模式/单例的常见实现 const myModule (function() { // 私有变量外部无法直接访问 let privateCounter 0; // 私有函数 function privateIncrement() { privateCounter; } // 返回一个公有API对象 return { increment() { privateIncrement(); console.log(Counter: ${privateCounter}); }, getCurrentCount() { return privateCounter; } }; })(); myModule.increment(); // “Counter: 1” myModule.increment(); // “Counter: 2” console.log(myModule.privateCounter); // undefined无法访问私有变量 console.log(myModule.getCurrentCount()); // 2通过公有接口访问核心思想通过一个立即执行函数表达式IIFE创建一个闭包在函数内部定义私有变量和函数然后返回一个包含公有方法的对象。这个返回的对象就是模块的公开接口也是唯一的实例单例。在现代开发中的应用在ES6模块import/export普及之前这种模式是组织前端代码、避免全局污染的主流方式。现在虽然原生模块已经很大程度上取代了它但在一些需要强封装、或在不支持ES6模块的环境如某些旧版浏览器直接运行脚本中这种模式依然有价值。此外它也是实现单例模式的经典方法。8. 终极选择如何根据场景挑选合适的工具了解了这么多创建对象的方式最后的问题来了在实际项目中我到底该用哪一种这里没有唯一的答案但有一些清晰的指导原则可以帮助你做出最佳选择。1. 简单数据记录、配置对象、函数参数 - 对象字面量{}理由语法最简洁性能通常最优意图最清晰。当你只是需要一个临时的、结构简单的键值对集合时这是不二之选。示例const config { apiUrl: ‘/endpoint’, timeout: 5000 };2. 需要明确类型标识和共享方法 - Class或构造函数模式理由class语法是现代标准提供了清晰的继承、静态方法、私有字段等特性工具链支持好。当你需要创建多个具有相同行为和状态的实例如用户、商品、组件时使用Class。示例定义User、Product、ModalDialog等类。3. 需要精确控制原型或创建无原型对象 - Object.create()理由这是最直接的原型操作工具。Object.create(null)创建纯净字典Object.create(proto)直接建立原型链在实现简单的对象组合或特定原型链结构时非常优雅。示例实现一个不继承任何Object.prototype方法的缓存对象创建一个以某个特定对象为原型的工具对象。4. 基于现有对象组合出新对象 - Object.assign()理由体现了“组合”思想将多个小功能对象Mixin合并灵活且耦合度低。适合构建功能可插拔的对象。示例一个游戏角色由CanMove、CanAttack、HasInventory等Mixin组合而成。5. 从结构化数据数组、Map转换 - Object.fromEntries()理由专为转换设计语义明确。当你从Map、URLSearchParams或二维数组等结构获得数据并需要转为普通对象时就用它。示例将API返回的键值对列表或URL查询参数快速转为配置对象。6. 工厂函数仍有其用武之地理由当对象的创建过程非常复杂涉及条件判断、异步操作、访问外部资源或者你不想暴露构造函数/类时工厂函数提供了很好的封装。它返回的对象可以是任何类型不一定是同一“类”灵活性最高。示例一个创建不同等级日志记录器的工厂createLogger(level)根据level返回ConsoleLogger、FileLogger等不同实现。性能考量 对于绝大多数应用这些创建方式的性能差异微乎其微不应成为首要考虑因素。V8等现代引擎对常见模式如字面量、Class做了大量优化。只有在极端性能敏感的场景如每秒创建数十万个对象才需要做细微的基准测试。通常代码的清晰度和可维护性远比那一点点性能差异重要。一个综合案例 假设你在构建一个UI库有一个基础的Widget类一个DraggableMixin对象和一个ResizableMixin对象。你可以这样设计// 基础类 class Widget { constructor(element) { this.element element; } render() { /* ... */ } } // 可混合的行为 const Draggable { dragStart() { /* ... */ }, dragMove() { /* ... */ } }; const Resizable { resizeStart() { /* ... */ }, resize() { /* ... */ } }; // 创建特定组件一个可拖动、可调整大小的对话框 function createDialog(content) { // 1. 创建基础Widget实例 const dialog new Widget(document.createElement(div)); dialog.content content; // 2. 混合行为 (使用Object.assign) Object.assign(dialog, Draggable, Resizable); // 3. 初始化混合行为可能需要的数据 dialog.isDragging false; dialog.isResizing false; return dialog; } const myDialog createDialog(Hello World); myDialog.render(); myDialog.dragStart(); // 来自Draggable在这个例子中我们结合了Class用于基础类型和共享方法、Object.assign用于组合功能和工厂函数用于封装复杂的创建逻辑。这比试图用一个深层次的继承链如DraggableResizableDialog extends DraggableDialog extends Dialog extends Widget要灵活和清晰得多。理解JavaScript中对象的各种创建方式本质上是理解这门语言“万物皆对象”和“基于原型”的核心哲学。从简单的字面量到强大的Class每一种方式都是工具箱里的一件利器。掌握它们你就能在面对不同的编程任务时游刃有余地选出最趁手的那一把。