JavaScript 深入之创建对象的多种方式以及优缺点
Category(分类): JavaScript Status: 已整理(2026)
原文文件只有标题、分类和来源链接,正文缺失。本文保留原始来源,并补回对象字面量、工厂函数、构造函数、原型、
Object.create、class、复制和组合等常见方式。性能结论不使用绝对化排名,应以实际场景和基准测试为准。
一、先明确“创建对象”在讨论什么
JavaScript 中的对象可以有:
- 自有属性和方法;
- 原型链;
- 属性描述符(可写、可枚举、可配置、getter/setter);
- 可调用能力(函数对象);
- 特定内建对象的内部槽,例如
Map、Set、Date和RegExp。
“对象创建方式”不只是在比较语法,还要考虑:
- 多个实例是否共享方法;
- 是否需要原型继承;
- 是否需要私有字段;
- 是否需要可变状态或生命周期;
- 是否需要字典对象、不可变快照或结构化复制;
- 团队是否能清晰维护这套抽象。
二、对象字面量:最直接的选择
对象字面量适合创建少量、结构明确的对象:
const user = {
name: 'Ada',
age: 36,
greet() {
return `Hello, ${this.name}`
}
}
console.log(user.greet()) // Hello, Ada
优点:
- 语法简洁、可读性好;
- 可以直接写方法、getter/setter、Symbol 键和计算属性名;
- 不需要额外的构造器或原型设置。
缺点:
- 每次写字面量都会创建一个新对象;
- 如果在循环中重复创建同样的方法函数,方法也会重复生成;
- 不适合表达复杂的实例生命周期或大量共享行为。
如果方法不依赖实例状态,也可以把它放到模块作用域或共享工具对象中,避免无意义地为每个对象创建函数。
三、工厂函数:用普通函数返回对象
工厂函数不要求调用者使用 new,可以根据输入返回不同结构:
function createUser(name, role = 'user') {
return {
name,
role,
canEdit() {
return this.role === 'admin'
}
}
}
const admin = createUser('Ada', 'admin')
const guest = createUser('Grace')
console.log(admin.canEdit()) // true
console.log(guest.canEdit()) // false
优点:
- 调用方式简单,不会因忘记
new而得到错误实例; - 可以返回不同类型的对象、缓存对象或已有对象;
- 适合模块封装、依赖注入和组合式 API。
缺点:
- 上例中的
canEdit每次调用都会创建一个新的函数; - 返回对象默认没有自定义构造器的原型关系,
instanceof不适合作为识别方式; - 需要共享方法时,应把方法放到共享对象、闭包外部或显式原型上。
共享方法的工厂写法:
const userMethods = {
canEdit() {
return this.role === 'admin'
}
}
function createUserWithSharedMethods(name, role = 'user') {
return Object.assign(Object.create(userMethods), { name, role })
}
const user = createUserWithSharedMethods('Ada', 'admin')
console.log(user.canEdit()) // true
console.log(Object.getPrototypeOf(user) === userMethods) // true
这种写法要注意:userMethods 是原型对象,修改它会影响所有实例;如果不希望共享可变状态,应冻结它或使用模块内不可变方法表。
四、构造函数与 prototype
传统构造函数使用 new 创建实例,并把共享方法放在构造函数的 prototype 上:
function User(name, role = 'user') {
this.name = name
this.role = role
}
User.prototype.canEdit = function () {
return this.role === 'admin'
}
const admin = new User('Ada', 'admin')
const guest = new User('Grace')
console.log(admin.canEdit()) // true
console.log(admin.canEdit === guest.canEdit) // true
console.log(admin instanceof User) // true
new User(...) 大致会:
- 创建一个新对象;
- 把新对象的原型关联到
User.prototype; - 以新对象作为
this执行User; - 如果构造函数没有显式返回对象,就返回这个新对象。
共享方法的优点是实例之间复用同一个函数;缺点是构造函数约定依赖 new,继承和 prototype.constructor 的维护容易出错,旧代码还经常把 this、严格模式和浏览器全局环境混在一起。
现代代码可以用 new.target 检查是否通过 new 调用,但通常更推荐 class 或明确的工厂函数:
function User(name) {
if (!new.target) {
throw new TypeError('User 必须使用 new 调用')
}
this.name = name
}
五、Object.create:明确指定原型
Object.create(proto) 会创建一个新对象,并把它的 [[Prototype]] 设置为 proto;它不会执行 proto 的构造函数,也不是深拷贝:
const userMethods = {
greet() {
return `Hello, ${this.name}`
}
}
const user = Object.create(userMethods)
user.name = 'Ada'
console.log(user.greet()) // Hello, Ada
console.log(Object.getPrototypeOf(user) === userMethods) // true
原型链查找与自有属性要区分:
console.log('greet' in user) // true:包含原型链
console.log(Object.hasOwn(user, 'greet')) // false:只检查自有属性
console.log(Object.hasOwn(user, 'name')) // true
也可以用属性描述符一次性定义自有属性:
const object = Object.create(userMethods, {
name: {
value: 'Ada',
enumerable: true,
writable: true,
configurable: true
}
})
5.1 Object.create(null) 字典
如果只需要字典,不希望对象继承 Object.prototype,可以使用空原型对象:
const dictionary = Object.create(null)
dictionary.key = 'value'
console.log(dictionary.key) // value
console.log(Object.getPrototypeOf(dictionary)) // null
console.log(Object.hasOwn(dictionary, 'key')) // true
它没有 toString、hasOwnProperty 等继承方法,序列化、调试和第三方库兼容性需要单独确认。使用 Map 存储任意键和值时,通常比把普通对象当作通用字典更直接:
const map = new Map()
map.set({ id: 1 }, 'value')
console.log(map.size) // 1
六、class:原型机制的现代语法
class 语法适合表达具有明确实例、方法和生命周期的模型:
class User {
#role
constructor(name, role = 'user') {
this.name = name
this.#role = role
}
canEdit() {
return this.#role === 'admin'
}
get role() {
return this.#role
}
}
const admin = new User('Ada', 'admin')
console.log(admin.canEdit()) // true
console.log(admin.role) // admin
console.log(Object.hasOwn(admin, 'canEdit')) // false:方法在 prototype 上
class 方法通常放在原型上,因此多个实例共享方法。它与旧式构造函数有相似的原型机制,但不能简单说成“完全只是语法糖”:
- class 构造器必须使用
new调用; - class 方法默认严格模式;
#private私有字段具有语言级的私有品牌检查;extends、super、静态初始化块和派生构造器有自己的语义;- 类声明不会像普通函数声明一样提升到整个作用域可用。
不要把私有字段和下划线约定混为一谈:
class Counter {
#value = 0
increment() {
this.#value += 1
}
get value() {
return this.#value
}
}
const counter = new Counter()
counter.increment()
console.log(counter.value) // 1
// counter.#value // SyntaxError:类外不能直接访问
七、继承与组合
继承可以复用原型方法,但不应为了复用少量代码而建立过深的层级:
class Animal {
speak() {
return 'sound'
}
}
class Dog extends Animal {
speak() {
return 'woof'
}
}
console.log(new Dog().speak()) // woof
组合通常更容易替换行为:
function createLogger(prefix) {
return {
log(message) {
console.log(`[${prefix}] ${message}`)
}
}
}
function createUserService(logger) {
return {
save(user) {
logger.log(`保存用户:${user.name}`)
return user
}
}
}
const service = createUserService(createLogger('user'))
service.save({ name: 'Ada' })
组合的优点是依赖显式、测试替换容易;继承适合确实存在稳定的“是一个”关系,并且子类需要遵守父类契约的场景。不要把 instanceof 当成唯一的类型系统。
八、复制对象不等于创建继承关系
对象展开和 Object.assign 都是浅复制:
const original = {
name: 'Ada',
profile: { city: 'London' }
}
const copyBySpread = { ...original }
const copyByAssign = Object.assign({}, original)
copyBySpread.profile.city = 'Paris'
console.log(original.profile.city) // Paris:嵌套对象仍共享
它们不会复制原型、私有字段、属性描述符或函数闭包,也不会把一个普通对象变成某个 class 的实例。如果需要保留特定原型,应明确使用 Object.create 或构造器;如果需要深层数据复制,可以根据类型和环境考虑 structuredClone(),但它也不是任意对象的通用复制方案:
const original = {
date: new Date('2020-01-01'),
values: new Map([['a', 1]])
}
const copy = structuredClone(original)
console.log(copy.date instanceof Date) // true
console.log(copy.values.get('a')) // 1
函数、DOM 节点、WeakMap 等不能按普通数据这样结构化克隆;需要复制业务模型时,应定义序列化边界。
九、原型操作与安全边界
读取原型使用 Object.getPrototypeOf(),创建指定原型的对象使用 Object.create()。不要把 __proto__ 当成首选 API:
const parent = { shared: true }
const child = Object.create(parent)
console.log(Object.getPrototypeOf(child) === parent) // true
Object.setPrototypeOf() 可以动态修改原型,但频繁修改已经运行中的对象可能影响引擎优化,也可能造成难以理解的继承关系:
const object = {}
Object.setPrototypeOf(object, parent)
console.log(object.shared) // true
优先在创建对象时确定原型;如果需要动态组合行为,通常使用组合、代理或显式委托。
十、如何选择
| 需求 | 推荐方式 | 需要注意 |
|---|---|---|
| 一个简单配置或数据对象 | 对象字面量 | 不要把临时数据强行建模成 class |
| 根据输入创建不同结构 | 工厂函数 | 共享方法、返回类型和缓存语义要明确 |
| 大量实例共享原型方法 | 构造函数或 class | 明确 new、继承和实例生命周期 |
| 明确指定原型或创建空原型字典 | Object.create | 它不执行构造函数,不是深拷贝 |
| 需要私有字段和类契约 | class | 方法通常在 prototype 上,类方法不会自动绑定实例 |
| 需要替换依赖和组合行为 | 工厂函数 + 组合 | 依赖显式,避免不必要的继承层次 |
| 需要任意键值映射 | Map | 不要把普通对象当作所有键类型的 Map |
不要笼统地说某种方式“性能最好”。对象形状、属性初始化顺序、原型层级、闭包数量、调用热点和垃圾回收都会影响结果;只有在真实工作负载和目标引擎中进行基准测试,才能得出有意义的结论。
十一、综合示例
class Repository {
#items = new Map()
save(item) {
this.#items.set(item.id, item)
}
find(id) {
return this.#items.get(id)
}
}
function createService(repository, logger = console) {
return {
add(item) {
repository.save(item)
logger.log('saved', item.id)
},
get(id) {
return repository.find(id)
}
}
}
const service = createService(new Repository())
service.add({ id: 1, name: 'Ada' })
console.log(service.get(1).name) // Ada
这里用 class 管理带私有状态的仓储,用工厂函数组合服务和日志依赖。它不是唯一答案,但每种创建方式的职责边界比较清晰。