
1. 对象创建从“为什么”开始在JavaScript的世界里对象是构建一切的基石。无论是前端页面上一个动态的按钮还是后端服务里一个承载用户信息的实体本质上都是一个对象。但很多开发者尤其是刚入门的同学常常会陷入一个误区只记住new Object()或者字面量{}这两种方式然后就开始写代码了。当遇到需要更精细控制对象行为、实现代码复用或者处理复杂原型链的场景时就会感到束手无策。我见过不少项目因为对象创建方式单一导致代码结构僵化难以维护。比如所有对象都用字面量创建当需要为几百个同类对象添加一个共享方法时就得一个个去改或者用蹩脚的方式去动态添加埋下了巨大的隐患。理解不同的创建方式不是为了炫技而是为了在合适的场景选择最合适的工具写出更健壮、更灵活、更易读的代码。今天我们就来彻底梳理一下JavaScript中创建对象的七种核心方式。我会从最基本的讲起一直深入到ES6的新特性并结合我这些年踩过的坑和总结的最佳实践让你不仅知道“怎么做”更明白“为什么这么做”以及“什么时候该用哪种”。2. 七种创建方式深度解析2.1 对象字面量最直接的起点对象字面量大概是所有JavaScript开发者最早接触也是最常用的创建方式。它的语法极其直观用一对花括号{}包裹起来的一组键值对。const person { name: 张三, age: 30, greet() { console.log(你好我是${this.name}); } };为什么它如此流行简洁性无需调用构造函数或任何函数直接定义一目了然。性能在大多数JavaScript引擎中字面量创建对象是性能最优的方式之一因为解析器可以对其进行高度优化。JSON兼容其语法是JSON格式的超集使得对象与JSON数据之间的转换几乎无缝。核心细节与注意事项属性名引号如果属性名包含空格、连字符或者以数字开头必须用引号括起来通常是字符串形式。例如{ ‘first-name’: ‘John’ }。方法简写ES6之后你可以用greet() {…}代替greet: function() {…}这让对象方法的定义更加简洁。计算属性名ES6引入了计算属性名允许你用方括号[]包裹一个表达式作为属性名。这在需要动态生成属性名时非常有用。const propKey ‘id’; const dynamicObj { [propKey ‘_123’]: ‘value’ // 属性名为 ‘id_123’ };注意字面量创建的对象其原型是Object.prototype。这意味着它自动拥有toString、valueOf等方法。但这也意味着如果你不小心修改了Object.prototype绝对不推荐这么做所有通过字面量创建的对象都会受到影响。这是一个需要警惕的全局污染问题。适用场景创建简单的、一次性的数据结构配置对象函数参数对象模块导出等。当对象不需要从某个特定“蓝图”实例化且结构相对固定时字面量是最佳选择。2.2 Object构造函数略显古老的方式使用new Object()是另一种基础方式但在现代开发中已较少直接使用。const person new Object(); person.name ‘张三’; person.age 30; person.greet function() { console.log(你好我是${this.name}); };为什么现在不常用了与对象字面量相比new Object()没有任何优势反而更冗长。它和字面量{}创建的对象在功能上完全等价原型也都是Object.prototype。字面量语法无论在可读性还是编写效率上都完胜。可以说new Object()是JavaScript早期为了语法完整性而存在的方式。一个仍有价值的用途Object构造函数本身作为一个工具对象提供了许多有用的静态方法如Object.create(),Object.assign(),Object.keys()等这些方法在日常开发中不可或缺。但用new来调用它创建对象确实可以放进历史博物馆了。2.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 person1 createPerson(‘张三’, 30); const person2 createPerson(‘李四’, 25);解决了什么问题它解决了批量创建相似对象的问题避免了代码重复。你可以把创建对象的逻辑集中在一个地方如果需要修改所有对象的初始化过程只需改这一个函数。核心缺陷对象识别问题工厂模式创建的对象其原型链顶端都是Object.prototype。person1和person2之间没有直接的原型链接我们无法通过instanceof这样的操作符来判断一个对象是否由某个“工厂”创建。它们只是碰巧有相同属性的普通对象。console.log(person1 instanceof createPerson); // false console.log(person1.constructor); // [Function: Object]适用场景在不需要关心对象具体“类型”即不依赖原型链和构造函数识别的简单场景下工厂模式是一种清晰的创建模式。例如创建一组配置项、一组UI组件的状态对象等。2.4 构造函数模式引入“类型”概念构造函数模式是JavaScript模拟面向对象“类”的第一次重要尝试。它通过一个普通的函数配合new操作符来创建对象。function Person(name, age) { this.name name; this.age age; this.greet function() { console.log(你好我是${this.name}); }; } const person1 new Person(‘张三’, 30); const person2 new Person(‘李四’, 25); console.log(person1 instanceof Person); // true console.log(person1.constructor Person); // truenew操作符背后发生了什么这是理解构造函数的关键。当你执行new Person(...)时引擎默默做了四件事在内存中创建一个全新的空对象。将这个新对象的内部[[Prototype]]即__proto__链接到Person.prototype。将构造函数内部的this绑定到这个新对象。执行构造函数内部的代码为this添加属性。如果构造函数没有显式返回一个对象则自动返回这个新创建的对象。解决了工厂模式的“对象识别”问题现在person1可以被识别为Person的“实例”因为它们共享同一个原型对象Person.prototype。核心缺陷方法重复创建仔细观察上面的代码person1.greet和person2.greet是两个不同的函数对象尽管它们功能完全相同。如果有1000个Person实例就会有1000个greet函数被创建这无疑是巨大的内存浪费。优化方案将方法移到原型上这正是JavaScript原型链设计的精妙之处。我们可以把共享的方法定义在构造函数的prototype属性上。function Person(name, age) { this.name name; this.age age; } Person.prototype.greet function() { console.log(你好我是${this.name}); }; const person1 new Person(‘张三’, 30); const person2 new Person(‘李四’, 25); console.log(person1.greet person2.greet); // true! 现在是同一个函数现在所有Person实例共享同一个greet方法内存效率大大提升。这也是构造函数模式最标准的用法。适用场景当你需要创建一种具有明确“类型”、且实例之间需要共享方法的对象时。它是ES6之前实现“类”的主要方式。2.5 原型模式极致的共享原型模式可以看作是对构造函数模式的一种极端运用它试图完全利用原型链来共享属性和方法。function Person() {} Person.prototype.name ‘默认姓名’; Person.prototype.age 0; Person.prototype.friends [‘李四’, ‘王五’]; Person.prototype.greet function() { console.log(你好我是${this.name}); }; const person1 new Person(); const person2 new Person(); console.log(person1.name); // ‘默认姓名’ console.log(person1.greet person2.greet); // true优点与风险并存它的优点是极致的内存节约所有实例完全共享原型上的属性。但这也正是它最大的问题引用类型的共享属性会互相影响。person1.friends.push(‘赵六’); console.log(person2.friends); // [‘李四’, ‘王五’, ‘赵六’] person2的friends也被修改了因为friends数组存在于Person.prototype上person1和person2访问的是同一个数组。这通常不是我们想要的行为。纯原型模式的适用场景非常有限通常只用于创建那些所有实例属性都完全一致且属性值为基本类型或不可变的引用类型的对象。在实践中它常与构造函数模式结合使用即组合模式构造函数内定义实例独有的属性原型上定义共享的方法。2.6 组合模式构造函数原型ES5时代的黄金标准组合模式结合了构造函数模式和原型模式两者的优点是ES6之前最主流、最推荐的创建自定义“类”的方式。// 构造函数定义实例属性 function Person(name, age) { this.name name; this.age age; this.friends [‘李四’, ‘王五’]; // 引用类型属性放在这里每个实例独立 } // 原型定义共享方法 Person.prototype.greet function() { console.log(你好我是${this.name}); }; const person1 new Person(‘张三’, 30); const person2 new Person(‘王五’, 28); person1.friends.push(‘赵六’); console.log(person1.friends); // [‘李四’, ‘王五’, ‘赵六’] console.log(person2.friends); // [‘李四’, ‘王五’] 不受影响 console.log(person1.greet person2.greet); // true方法共享为什么它是黄金标准实例属性独立每个对象有自己的name,age,friends副本互不干扰。方法高效共享所有实例共享greet等方法节省内存。对象识别清晰instanceof和constructor都能正确工作。这种模式清晰地区分了“实例成员”和“原型成员”逻辑分明几乎满足了当时所有面向对象编程的需求。直到今天很多库和遗留代码依然大量使用这种模式。2.7 Object.create()基于原型的纯净创建Object.create()方法提供了一种非常灵活且纯粹的原型式对象创建方式。它允许你明确指定新创建对象的原型。const personPrototype { greet() { console.log(你好我是${this.name}); } }; const person1 Object.create(personPrototype); person1.name ‘张三’; // 后续添加属性 person1.age 30; const person2 Object.create(personPrototype, { name: { value: ‘李四’, writable: true, enumerable: true, configurable: true }, age: { value: 25 } }); person1.greet(); // 你好我是张三 console.log(Object.getPrototypeOf(person1) personPrototype); // true核心原理Object.create(proto, [propertiesObject])第一个参数proto新对象的原型。可以是任意对象或null。第二个参数propertiesObject可选一个对象其自身的可枚举属性将作为新对象的属性描述符被添加。你可以在这里精细控制每个属性的特性value,writable,enumerable,configurable。与new构造函数的本质区别new Constructor()创建的对象原型是Constructor.prototype并且会自动执行Constructor函数来初始化。Object.create(proto)创建的对象原型就是proto不会自动执行任何初始化函数。对象的初始化完全由开发者手动控制。强大之处创建纯净对象和实现继承创建没有原型的对象Object.create(null)。这个对象完全是一个“白板”不继承Object.prototype的任何方法如toString,hasOwnProperty。这在需要绝对干净的数据字典或映射或者作为键值对集合时非常有用可以避免与原型上的属性名冲突。实现原型式继承这是Object.create()最经典的用途也是ES5实现继承的基石。function Animal(name) { this.name name; } Animal.prototype.speak function() {}; function Dog(name, breed) { Animal.call(this, name); // 继承实例属性 this.breed breed; } // 继承原型方法将Dog.prototype的原型设置为Animal.prototype Dog.prototype Object.create(Animal.prototype); // 修复constructor指向 Dog.prototype.constructor Dog; Dog.prototype.bark function() { console.log(‘汪汪’); };适用场景当你需要精确控制对象的原型链或者实现灵活的、非基于“类”构造函数的继承关系时。它也常用于创建行为委托Behavior Delegation模式的对象。2.8 ES6 Class语法糖但不止于糖ES6引入的class关键字本质上是上述组合模式构造函数原型的语法糖但它提供了更清晰、更接近传统面向对象语言的语法。class Person { constructor(name, age) { this.name name; this.age age; this.friends [‘李四’, ‘王五’]; } // 方法自动被添加到 Person.prototype greet() { console.log(你好我是${this.name}); } // 静态方法存在于 Person 本身 static describe() { console.log(‘这是一个Person类’); } // Getter/Setter get introduction() { return ${this.name}, ${this.age}岁; } } const person1 new Person(‘张三’, 30); const person2 new Person(‘王五’, 28); person1.greet(); // 你好我是张三 Person.describe(); // 这是一个Person类 console.log(person1.introduction); // 张三, 30岁 console.log(typeof Person); // ‘function’类本质还是函数 console.log(person1 instanceof Person); // true它解决了什么统一的、声明式的语法将构造函数、原型方法、静态方法等集中在一个class声明块中结构一目了然。内置的new检查如果你尝试像普通函数一样调用Person()会直接抛出TypeError避免了因忘记new导致的this指向全局对象的经典错误。更优雅的继承通过extends和super关键字实现继承变得非常简单直观。class Student extends Person { constructor(name, age, grade) { super(name, age); // 必须首先调用super() this.grade grade; } study() { console.log(${this.name}正在学习); } }支持类字段声明ES2022正式化可以在类顶层直接声明实例属性或静态属性使代码更简洁。class Person { nationality ‘中国’; // 实例字段 static species ‘Human’; // 静态字段 constructor(name) { this.name name; } }注意事项与常见误区类声明不会提升虽然函数声明会提升但class声明不会。你不能在声明之前使用类但类表达式也有其作用域规则。类内部默认是严格模式在类声明和类表达式的代码体中默认启用严格模式。方法不可枚举类中定义的原型方法默认是不可枚举的enumerable: false这与在原型上直接通过赋值定义的方法不同后者默认可枚举。这在使用for...in循环时需要注意。没有真正的“私有属性”ES2022前在ES2022引入#私有字段之前类内部属性默认是公开的。通常通过命名约定如_privateProp或利用WeakMap、Symbol来模拟私有性。适用场景在现代JavaScript开发中只要是创建需要实例化、有明确“类”概念的对象class语法都是首选。它语法清晰工具支持好如类型检查、IDE提示并且是语言标准的发展方向。3. 核心环节如何选择与实战要点了解了七种武器关键在于如何根据战场场景选择最趁手的那一把。这里没有银弹只有最适合。3.1 选择策略速查表创建方式核心特点优点缺点典型应用场景对象字面量{}直接定义原型为Object.prototype最简洁、性能好、JSON友好无法直接实现结构复用一次性数据结构、配置对象、模块导出、函数参数new Object()构造函数调用与字面量功能相同语法冗余无优势基本不再使用了解即可工厂模式函数封装创建过程封装创建逻辑避免重复代码对象类型识别困难instanceof无效创建一组结构相似但无需类型识别的对象构造函数模式new 普通函数原型链识别类型解决了对象识别问题可创建自定义“类型”每个实例的方法都是独立的浪费内存需要类型识别但方法较少的简单对象需结合原型优化原型模式完全依赖原型共享属性极致的内存共享共享的引用类型属性会被所有实例修改风险高极少单独使用通常与构造函数组合组合模式构造函数定义实例属性 原型定义方法实例属性独立方法共享内存高效类型识别清晰ES5语法下略显繁琐ES6之前创建自定义类型的标准方式Object.create()直接指定原型对象创建原型链控制极其灵活可创建无原型对象无自动初始化需手动添加属性纯净对象、灵活的原型式继承、行为委托模式ES6 Class组合模式的语法糖声明式语法语法清晰统一内置new检查继承优雅工具链支持好本质上仍是原型和构造函数现代开发首选创建任何需要“类”概念的对象3.2 实战中的关键细节与避坑指南1. 关于constructor属性的维护当你手动修改一个对象的原型例如Dog.prototype Object.create(Animal.prototype)原有的constructor属性会丢失转而指向父类的构造函数。这有时会导致依赖constructor属性的代码出错。好的实践是手动修复它Dog.prototype Object.create(Animal.prototype); Dog.prototype.constructor Dog; // 修复constructor指向在ES6 Class的继承中这个过程是自动完成的。2. 方法定义箭头函数与普通函数在类或构造函数中定义方法时务必注意this的指向。class ProblematicComponent { constructor() { this.value 42; } handleClick() { // 当此方法作为事件监听器被调用时this可能指向DOM元素而不是实例 console.log(this.value); // 可能是 undefined! } }解决方案在构造函数中绑定this.handleClick this.handleClick.bind(this);使用类字段箭头函数推荐箭头函数没有自己的this会捕获定义时的this。class GoodComponent { value 42; handleClick () { console.log(this.value); // 永远指向实例 }; }注意使用箭头函数定义的方法不在原型上而是每个实例都会创建自己的函数副本。这牺牲了一点内存但换来了this绑定的便利和正确性。对于小型组件或方法不多的类这是可接受的权衡。3. 对象属性的枚举、配置与冻结JavaScript对象的属性有三个关键特性除了valueenumerable可枚举是否会在for...in循环或Object.keys()中出现。writable可写是否可以修改。configurable可配置是否可以删除或修改这些特性。使用Object.defineProperty()可以精细控制这些特性。Object.freeze()可以冻结一个对象使其完全不可变浅冻结。Object.seal()可以密封一个对象防止添加/删除属性但允许修改现有属性。在创建配置对象、常量对象或需要保护核心数据时这些API非常有用。4. 性能考量对于需要创建海量成千上万简单对象的场景创建方式的性能差异会被放大。**字面量{}和Object.create()**通常是性能最好的。构造函数new由于涉及原型链查找和函数调用稍慢但在绝大多数应用中可忽略不计。Class语法在底层与构造函数模式等效性能一致。真正的性能瓶颈往往不在于创建方式本身而在于不当的设计比如在循环中重复创建大量闭包、在原型上定义过大的数组或对象等。4. 常见问题与排查实录在实际开发中关于对象创建的问题层出不穷。这里记录几个我反复遇到的典型问题。问题一Uncaught TypeError: XXX is not a constructor现象使用new调用一个函数时报错。排查检查你new的是否是一个真正的函数。可能是变量名拼写错误或者该变量当前的值不是函数例如是undefined或一个普通对象。如果你new的是一个箭头函数一定会报这个错因为箭头函数不能作为构造函数。在ES6 Class中如果你在子类的constructor中没有在this之前调用super()也会触发类似错误。解决仔细检查变量和函数名确认其类型。对于类确保构造函数格式正确。问题二方法中的this指向意外丢失现象在对象方法内访问this.property时得到undefined。场景最常见于将对象方法作为回调函数传递时如事件监听器、setTimeout、Promise.then。const obj { data: ‘hello’, logData() { console.log(this.data); } }; setTimeout(obj.logData, 100); // 输出 undefined因为此时this指向全局或undefined(严格模式)解决绑定thissetTimeout(obj.logData.bind(obj), 100);使用箭头函数包装setTimeout(() obj.logData(), 100);使用类字段箭头函数如前文所述。问题三修改原型对象对现有实例的影响现象在创建实例后向构造函数的原型添加新方法现有实例也能立即访问到。function Person(name) { this.name name; } const alice new Person(‘Alice’); Person.prototype.sayHi function() { console.log(‘Hi!’); }; alice.sayHi(); // 输出 ‘Hi!’ 可以访问原理实例与原型之间是动态链接。访问实例属性时如果实例自身没有引擎会沿着__proto__链去原型上找。所以修改原型会影响所有基于该原型的实例。注意反之如果你完全重写替换了整个原型对象现有实例的原型链不会更新它们仍然指向旧的原型对象。只有新创建的实例才会链接到新的原型。const bob new Person(‘Bob’); Person.prototype { newMethod() {} }; bob.sayHi(); // 仍然输出 ‘Hi!’ (来自旧原型) const charlie new Person(‘Charlie’); charlie.sayHi(); // TypeError: charlie.sayHi is not a function charlie.newMethod(); // 可以访问因此应避免直接替换整个prototype对象而是选择在原有原型上添加或修改属性。问题四for...in循环遍历出意外属性现象使用for...in遍历对象时不仅遍历了自身属性还遍历了原型链上的可枚举属性。解决在循环体内总是使用obj.hasOwnProperty(key)进行过滤确保只处理对象自身的属性。for (let key in obj) { if (obj.hasOwnProperty(key)) { // 处理自身属性 } }或者更现代的做法是直接使用Object.keys(obj)它只返回对象自身的可枚举属性名数组。理解JavaScript对象的创建方式是深入理解这门语言原型继承体系的关键。从简单的字面量到灵活的Object.create()再到现代的class语法每一种方式都体现了语言在不同发展阶段的设计思想。我的经验是在新项目中对于复杂的业务对象毫不犹豫地使用class对于简单的数据聚合使用字面量当需要极致的控制或实现特殊的继承模式时请想起Object.create()。记住没有最好的只有最合适的。多写多思考当你能根据场景本能地选出最合适的方式时你就真正掌握了JavaScript对象的核心。