JavaScript:构造函数
Category(分类): JavaScript Status: 已整理(2026)
原文件只有 CSDN 来源链接。本文补回传统构造函数、
prototype、new、new.target、class、继承和工厂函数内容,并明确区分“可调用”和“可构造”。原文中的window、函数重复创建和new.target判断等旧说法按现代 ECMAScript 语义修正。
一、什么是构造函数
在 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是实例,自有属性是name和age;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 做了什么
下面的过程是便于理解的简化描述:
- 检查右侧对象是否可构造;
- 根据
newTarget.prototype(不是简单地永远取左侧构造函数)创建实例原型;如果该值不是对象,规范会回退到内建默认原型; - 用新对象作为
this执行构造函数; - 对基础构造器,如果显式返回对象则使用该对象,否则返回新实例;派生 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]]、newTarget 和 OrdinaryCreateFromConstructor 等抽象操作。上面的四步是教学模型,不意味着可以用几行普通 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 创建并绑定了实例
“普通函数调用时 this 是 window”并不是通用规则:
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。