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=小明&age=25'); 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,每一种方式都是工具箱里的一件利器。掌握它们,你就能在面对不同的编程任务时,游刃有余地选出最趁手的那一把。