技术知识文章集合TECHNICAL ARCHIVE · 457 DOCUMENTS

显示模式

登录
ARCHIVE DOCUMENTJS

JavaScript 深入之创建对象的多种方式以及优缺点

所属馆藏
JavaScript
文件格式
Markdown
原始路径
JavaScript/33-JavaScript深入之创建对象的多种方式以及优缺点
本文目录12 个章节
  1. 一、先明确“创建对象”在讨论什么
  2. 二、对象字面量:最直接的选择
  3. 三、工厂函数:用普通函数返回对象
  4. 四、构造函数与 prototype
  5. 五、Object.create:明确指定原型
  6. 六、class:原型机制的现代语法
  7. 七、继承与组合
  8. 八、复制对象不等于创建继承关系
  9. 九、原型操作与安全边界
  10. 十、如何选择
  11. 十一、综合示例
  12. 十二、参考资料与原文

JavaScript 深入之创建对象的多种方式以及优缺点

Category(分类): JavaScript Status: 已整理(2026)

原文文件只有标题、分类和来源链接,正文缺失。本文保留原始来源,并补回对象字面量、工厂函数、构造函数、原型、Object.createclass、复制和组合等常见方式。性能结论不使用绝对化排名,应以实际场景和基准测试为准。

一、先明确“创建对象”在讨论什么

JavaScript 中的对象可以有:

  • 自有属性和方法;
  • 原型链;
  • 属性描述符(可写、可枚举、可配置、getter/setter);
  • 可调用能力(函数对象);
  • 特定内建对象的内部槽,例如 MapSetDateRegExp

“对象创建方式”不只是在比较语法,还要考虑:

  • 多个实例是否共享方法;
  • 是否需要原型继承;
  • 是否需要私有字段;
  • 是否需要可变状态或生命周期;
  • 是否需要字典对象、不可变快照或结构化复制;
  • 团队是否能清晰维护这套抽象。

二、对象字面量:最直接的选择

对象字面量适合创建少量、结构明确的对象:

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(...) 大致会:

  1. 创建一个新对象;
  2. 把新对象的原型关联到 User.prototype
  3. 以新对象作为 this 执行 User
  4. 如果构造函数没有显式返回对象,就返回这个新对象。

共享方法的优点是实例之间复用同一个函数;缺点是构造函数约定依赖 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

它没有 toStringhasOwnProperty 等继承方法,序列化、调试和第三方库兼容性需要单独确认。使用 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 私有字段具有语言级的私有品牌检查;
  • extendssuper、静态初始化块和派生构造器有自己的语义;
  • 类声明不会像普通函数声明一样提升到整个作用域可用。

不要把私有字段和下划线约定混为一谈:

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 管理带私有状态的仓储,用工厂函数组合服务和日志依赖。它不是唯一答案,但每种创建方式的职责边界比较清晰。

十二、参考资料与原文

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

支持搜索文章标题、所属分类和原始文档路径。

按分类浏览

10 COLLECTIONS