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

显示模式

登录
ARCHIVE DOCUMENTJS

JavaScript:构造函数

所属馆藏
JavaScript
文件格式
Markdown
原始路径
JavaScript/38-JavaScript:构造函数
本文目录13 个章节
  1. 一、什么是构造函数
  2. 二、传统构造函数的写法
  3. 三、new 做了什么
  4. 四、prototype 与原型链
  5. 五、使用 new.target
  6. 六、class 是现代构造器语法
  7. 七、构造函数、工厂函数和 Object.create
  8. 八、Reflect.construct 与动态构造
  9. 九、构造函数与 this
  10. 十、继承、原型与组合的取舍
  11. 十一、如何判断实例
  12. 十二、总结
  13. 参考资料

JavaScript:构造函数

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

原文件只有 CSDN 来源链接。本文补回传统构造函数、prototypenewnew.target、class、继承和工厂函数内容,并明确区分“可调用”和“可构造”。原文中的 window、函数重复创建和 new.target 判断等旧说法按现代 ECMAScript 语义修正。

原文:JavaScript:构造函数

一、什么是构造函数

在 JavaScript 中,“构造函数”不是只由函数名首字母大写决定的身份,而是指一个可以通过 new 参与构造过程的可构造对象。更准确地说,运行时对象可能具有两个不同能力:

  • 可调用(callable):可以使用 fn() 执行;
  • 可构造(constructable):可以使用 new fn() 创建实例。

普通函数通常同时具备这两种能力,但箭头函数、对象方法、async 函数和 generator 函数不能作为构造函数:

function OrdinaryFunction() {}
const arrowFunction = () => {}
const objectMethod = { method() {} }.method
async function asyncFunction() {}
function* generatorFunction() {}

console.log(typeof OrdinaryFunction) // function
console.log(typeof arrowFunction) // function

console.log(new OrdinaryFunction() instanceof OrdinaryFunction) // true

for (const value of [arrowFunction, objectMethod, asyncFunction, generatorFunction]) {
  try {
    Reflect.construct(value, [])
  } catch (error) {
    console.log(error.name) // TypeError
  }
}

typeof value === 'function' 只能说明它具有函数标签,不能单独证明它可以被 new,甚至不能保证可以用普通调用执行:class 构造器的 typeof 也是 "function",但直接调用会抛出 TypeError

class ClassConstructor {}

console.log(typeof ClassConstructor) // function
try {
  ClassConstructor()
} catch (error) {
  console.log(error.name) // TypeError
}

二、传统构造函数的写法

function User(name, age) {
  this.name = name
  this.age = age
}

User.prototype.greet = function () {
  return `Hello, ${this.name}`
}

const user = new User('Ada', 36)

console.log(user.name) // Ada
console.log(user.greet()) // Hello, Ada
console.log(user instanceof User) // true
console.log(Object.getPrototypeOf(user) === User.prototype) // true

这里:

  • User 是一个函数对象;
  • User.prototype 是构造实例默认使用的原型对象;
  • user 是实例,自有属性是 nameage
  • greet 通常只创建一份并被实例共享;
  • user[[Prototype]] 指向 User.prototype

首字母大写是约定,帮助调用者知道应该使用 new,但它不是引擎识别构造函数的依据。

2.1 不要在构造函数中重复创建方法

下面的写法会为每个实例创建一个新函数:

function BadUser(name) {
  this.name = name
  this.greet = function () {
    return `Hello, ${this.name}`
  }
}

const first = new BadUser('Ada')
const second = new BadUser('Grace')
console.log(first.greet === second.greet) // false

如果方法不需要实例私有状态,优先放到 prototype 或 class 方法中;如果确实需要闭包捕获私有数据,再接受每个实例创建函数的成本。

三、new 做了什么

下面的过程是便于理解的简化描述:

  1. 检查右侧对象是否可构造;
  2. 根据 newTarget.prototype(不是简单地永远取左侧构造函数)创建实例原型;如果该值不是对象,规范会回退到内建默认原型;
  3. 用新对象作为 this 执行构造函数;
  4. 对基础构造器,如果显式返回对象则使用该对象,否则返回新实例;派生 class 构造器对非 undefined 的原始返回值会抛出 TypeError
function Product(name) {
  this.name = name
}

const product = new Product('book')
console.log(product.name) // book
console.log(Object.getPrototypeOf(product) === Product.prototype) // true

规范内部会使用 [[Construct]]newTargetOrdinaryCreateFromConstructor 等抽象操作。上面的四步是教学模型,不意味着可以用几行普通 JavaScript 完全复制引擎的内部过程。

3.1 构造函数的返回值

返回原始值时,原始值会被忽略:

function ReturnsPrimitive() {
  this.value = 1
  return 2
}

const primitiveResult = new ReturnsPrimitive()
console.log(primitiveResult.value) // 1
console.log(primitiveResult instanceof ReturnsPrimitive) // true

返回对象时,对象会替代默认实例:

function ReturnsObject() {
  this.value = 1
  return { value: 2 }
}

const objectResult = new ReturnsObject()
console.log(objectResult.value) // 2
console.log(objectResult instanceof ReturnsObject) // false

基础构造器和派生 class 的返回规则不能混为一谈:

class Base {
  constructor() {
    return 1 // 基础 class 构造器会忽略原始值
  }
}

class Derived extends Base {
  constructor() {
    super()
    return 1 // 派生构造器返回原始值会失败
  }
}

console.log(new Base() instanceof Base) // true
try {
  new Derived()
} catch (error) {
  console.log(error.name) // TypeError
}

这个特性在工厂构造器中可能有用,但也会让代码难以理解,不应滥用。

四、prototype 与原型链

构造函数的 .prototype 与实例的 [[Prototype]] 是两个有关联但不同的概念:

function User() {}
const user = new User()

console.log(user.__proto__ === User.prototype) // true:legacy 访问器,仅作历史示例
console.log(Object.getPrototypeOf(user) === User.prototype) // true:推荐写法
console.log(Object.getPrototypeOf(User) === Function.prototype) // 通常为 true:函数对象自己的原型链
  • User.prototype 是一个普通对象,默认有 constructor 属性指回 User
  • user 的原型通常是 User.prototype
  • User 本身作为函数对象的原型通常是 Function.prototype
  • Object.getPrototypeOf(User)User.prototype 不是同一个概念。

访问原型应使用:

Object.getPrototypeOf(user)
Object.setPrototypeOf(user, anotherPrototype)
Object.create(User.prototype)

__proto__ 是历史访问器,不建议作为通用读写 API;频繁调用 Object.setPrototypeOf 还可能影响引擎优化和对象形状,应优先在创建时确定原型。

4.1 覆写 prototype 的陷阱

function A() {}
A.prototype = {
  greet() {
    return 'hello'
  }
}

const a = new A()
console.log(a.constructor === A) // false:默认 constructor 属性被替换了

如果确实需要整体替换原型,应显式恢复 constructor,并设置合理的属性描述符:

function B() {}
B.prototype = Object.create(Object.prototype, {
  constructor: {
    value: B,
    writable: true,
    configurable: true,
    enumerable: false
  },
  greet: {
    value() {
      return 'hello'
    },
    writable: true,
    configurable: true,
    enumerable: false
  }
})

现代代码通常使用 class,避免手动重建原型描述符。

五、使用 new.target

new.target 在函数作为构造器调用时指向实际的 new target;普通调用时为 undefined。如果只是阻止忘记 new,应检查 !new.target

function User(name) {
  if (!new.target) {
    throw new TypeError('User 必须使用 new 调用')
  }
  this.name = name
}

const user = new User('Ada')
console.log(user.name) // Ada

不要写成 new.target !== User 来判断是否使用了 new

function Base() {
  console.log(new.target?.name)
}

function Sub() {
  Base.call(this)
}

Object.setPrototypeOf(Sub, Base)
// new Sub() 中 Base.call(this) 并不会自动让 Base 的 new.target 变成 Sub;
// 派生 class 则有不同的 super 构造语义。

new.target 还可用于抽象基类检查:

class AbstractRepository {
  constructor() {
    if (new.target === AbstractRepository) {
      throw new TypeError('AbstractRepository 不能直接实例化')
    }
  }
}

class UserRepository extends AbstractRepository {}
const repository = new UserRepository()
console.log(repository instanceof UserRepository) // true

六、class 是现代构造器语法

class User {
  #role

  constructor(name, role = 'user') {
    this.name = name
    this.#role = role
  }

  canEdit() {
    return this.#role === 'admin'
  }

  static guest() {
    return new User('Guest')
  }
}

const admin = new User('Ada', 'admin')
console.log(admin.canEdit()) // true
console.log(User.guest().name) // Guest
console.log(Object.hasOwn(admin, 'canEdit')) // false:方法在 User.prototype

class 与传统构造函数共享原型机制,但有重要语义差异:

  • class 构造器必须使用 new
  • class 方法默认处于严格模式;
  • #private 字段不能从类外直接访问;
  • class 声明不会像普通函数声明一样按相同规则提升;
  • 派生 class 的构造器必须先调用 super(),之后才能使用 this

6.1 派生 class 与 super

class Animal {
  constructor(name) {
    this.name = name
  }

  speak() {
    return `${this.name} makes a sound`
  }
}

class Dog extends Animal {
  constructor(name, breed) {
    super(name)
    this.breed = breed
  }

  speak() {
    return `${super.speak()} and barks`
  }
}

const dog = new Dog('Lucky', 'Corgi')
console.log(dog.speak()) // Lucky makes a sound and barks
console.log(dog instanceof Animal) // true

super() 不只是调用一个普通函数;它参与派生构造器的初始化和 new.target 语义。不要用 Base.call(this, ...) 完全替代 class 的 super()

七、构造函数、工厂函数和 Object.create

7.1 构造函数适合什么

构造函数或 class 适合:

  • 需要大量实例共享原型方法;
  • 需要明确的实例身份和生命周期;
  • 需要继承、私有字段或静态方法;
  • 使用方接受 new 这一构造语义。

7.2 工厂函数适合什么

工厂函数不需要 new,可以返回不同类型或缓存对象:

function createTransport(type) {
  if (type === 'memory') {
    return {
      send(message) {
        return `memory: ${message}`
      }
    }
  }

  if (type === 'console') {
    return {
      send(message) {
        console.log(message)
      }
    }
  }

  throw new RangeError(`未知传输类型:${type}`)
}

console.log(createTransport('memory').send('hello')) // memory: hello

工厂不适合被强行识别为某个 class;如果需要类型检测,应设计明确的能力接口或品牌字段。

7.3 Object.create

Object.create(proto) 创建对象并设置原型,但不会调用构造函数:

const userMethods = {
  greet() {
    return `Hello, ${this.name}`
  }
}

const user = Object.create(userMethods)
user.name = 'Ada'
console.log(user.greet()) // Hello, Ada

如果使用 Object.create(null) 创建字典,它不会继承 Object.prototype

const dictionary = Object.create(null)
dictionary.key = 'value'
console.log(Object.hasOwn(dictionary, 'key')) // true

八、Reflect.construct 与动态构造

需要把构造器和参数列表作为变量传递时,可以使用 Reflect.construct

class User {
  constructor(name, age) {
    this.name = name
    this.age = age
  }
}

const args = ['Ada', 36]
const user = Reflect.construct(User, args)
console.log(user instanceof User) // true
console.log(user.name) // Ada

第三个参数可以指定 newTarget,这在代理构造或继承框架中有用:

function Base(name) {
  this.name = name
}

function Derived() {}
Derived.prototype = Object.create(Base.prototype)
Derived.prototype.constructor = Derived

const object = Reflect.construct(Base, ['Ada'], Derived)
console.log(object instanceof Derived) // true

Function.prototype.call/apply/Reflect.apply 只执行 [[Call]],不能替代构造过程:

class User {
  constructor(name) {
    this.name = name
  }
}

try {
  Reflect.apply(User, {}, ['Ada'])
} catch (error) {
  console.log(error.name) // TypeError
}

九、构造函数与 this

普通函数的 this 取决于调用形式,构造调用是其中一种特殊形式:

function User(name) {
  this.name = name
}

const object = {}
User.call(object, 'Ada')
console.log(object.name) // Ada:这是普通函数调用,不是 new

const user = new User('Grace')
console.log(user.name) // Grace:new 创建并绑定了实例

“普通函数调用时 thiswindow”并不是通用规则:

function readThis() {
  return this
}

function readStrictThis() {
  'use strict'
  return this
}

console.log(readStrictThis()) // undefined
console.log(readStrictThis.call(null)) // null

浏览器经典脚本、ES module、Node.js CommonJS 和 Worker 的顶层环境不同;模块和 class 默认严格模式。

十、继承、原型与组合的取舍

原型继承适合稳定的“是一个”关系:

class Vehicle {
  move() {
    return 'moving'
  }
}

class Car extends Vehicle {
  drive() {
    return this.move()
  }
}

console.log(new Car().drive()) // moving

但很多业务代码更适合组合:

function createLogger(prefix) {
  return {
    log(message) {
      console.log(`[${prefix}] ${message}`)
    }
  }
}

function createService({ logger }) {
  return {
    run() {
      logger.log('service running')
    }
  }
}

createService({ logger: createLogger('user') }).run()

组合的依赖更加显式,测试时也容易替换。不要为了复用一个方法建立很深的原型层级。

十一、如何判断实例

instanceof 检查的是右侧构造器的 prototype 是否出现在左侧对象的原型链上:

class User {}
const user = new User()

console.log(user instanceof User) // true
console.log(user instanceof Object) // true
console.log(Object.hasOwn(user, 'constructor')) // false

它会受到跨 Realm、原型重分配、Proxy 和 Symbol.hasInstance 的影响。instanceof 适合同一运行环境中可信 class 的普通判断,不是通用的序列化类型标签。

不要依赖 constructor 属性判断类型:

const object = new User()
object.constructor = null
console.log(object instanceof User) // true

即使 constructor 被覆盖,原型链仍然存在;反过来,伪造 constructor 也不能创建真实实例品牌。

十二、总结

  • 构造函数的核心不是首字母大写,而是对象是否具有 [[Construct]]
  • 函数可以可调用、可构造,二者是不同能力;
  • 箭头函数、对象方法、async/generator 函数不能使用 new
  • new 会建立实例原型、绑定 this、执行构造逻辑并处理对象返回值;
  • Ctor.prototype 是实例默认原型,Object.getPrototypeOf(Ctor) 是构造函数自身的原型链,二者不要混淆;
  • 构造函数中的方法通常应放到 prototype 或 class 方法上,避免每个实例重复创建;
  • new.target 可以区分构造调用和普通调用,但不能用简单比较覆盖所有继承语义;
  • class 提供现代的构造、继承、私有字段和静态成员语法;
  • 工厂函数、Object.create 和组合是构造函数之外的有效方案;
  • call/apply 不能替代 new,动态构造应使用 Reflect.construct

参考资料

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

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

按分类浏览

10 COLLECTIONS