值 vs 类型:为什么 Object 不是 Object 类型???

331 阅读6分钟

00. Hello World

大家好,我是大家的 林语冰

有道是,JS 中万物皆对象,结果最近学 TS 偏偏被对象搞晕了。

在本文中,我们会结合 TS 源码,深度学习 Object 相关的技术细节:

  1. Object = JS 构造函数 + TS 接口
  2. 为什么 Object 不是 Object 类型
  3. 一个类的两种类型

01. JS 值 vs TS 类型

在 TS 中,一个 Object 表示两种编程概念:

  • 一个值:运行时的 JS 构造函数
  • 一种类型:编译时的 TS 接口

00-type.png

由于 Object 接口和 Object() 构造函数同名,这有时容易混淆:

let o1: Object = new Object()
// 或者
let o2: Object = Object.create(Object)

上述例子中,变量声明左侧的 Object 表示一种类型(TS 接口),右侧的 Object 表示一个值(JS 构造函数)。

我们一般会根据经验法则来区分 —— Object 位于类型注解中则推断它是 TS 接口,位于表达式中推断它是 JS 构造函数。

但实际开发的代码可能会反直觉:

let o1: InstanceType<typeof Object> = new Object()
// 这里的 Object 都表示值

let o2: Object = (o1 as Object).valueOf()
// 这里的 Object 都表示类型

当你对值或类型感到困惑时,一种想象练习是根据实际上下文去拆解:

let o1: InstanceType<typeof Object> = new Object()

// 上面代码可以拆解为:
let OValue = Object
type OType = Object

let o2: InstanceType<typeof OValue> = new OValue() // ✅
let o3: InstanceType<typeof OType> // ❌

这样,我们可以直观地分辨 Object 到底是用作 TS 类型,还是用作 JS 值。

👉 请记住,值和类型不是一回事。Object 的不同上下文和用法决定了它是作为值,还是作为类型。

02. Object 不是 Object 类型

JS 可以使用 typeof 来查询运行时类型,同理,TS 也支持使用 typeof[1] 来查询编译时类型:

let type = typeof Object
// => 'function',运行时类型

type T = typeof Object
// => ObjectConstructor,编译时类型

如上所示,Object 其实 不是 Object 类型,即使两者的拼写一模一样。

根据《ES2025 语言规范》[2],函数本质上是支持 #Call 内部方法的可调用对象:

'call' in Object // true
Object instanceof Object // true

// 函数字面量类型:
type Fn1 = (value: any) => any
// 这等价于函数对象类型:
type Fn2 = {
  (value: any): any
  // ...
}

👇 上述代码说明了:

  • 函数是包含 #Call 内部方法的对象,Object() 函数是 Object 的派生实例。
  • 函数类型是包含调用签名的对象类型,函数类型字面量是只包含单个调用签名的对象类型的简写。

作为可调用对象,Object() 构造函数既可以包含 Object.prototype.hasOwnProperty() 等继承属性,也包含 Object.hasOwn() 等自有属性,后者无法被 Object 接口描述。

为此,TS 源码提供了内置的 ObjectConstructor 接口[3] 来描述 Object() 函数:

interface ObjectConstructor {
  // 1. 调用签名
  (value: any): any
  // 2. 构造签名
  new (value?: any): Object
  // 3. 其他属性...
  create(o: object | null): any
}

declare var Object: ObjectConstructor

可以看到,ObjectConstructor 的定义包括三个部分:

  1. 作为函数:调用签名
  2. 作为构造函数:构造签名
  3. 作为对象:其他属性签名

02-octor.png

此外,new Object() 返回的 Object 实例类型也是 TS 的内置接口:

// es5.d.ts
interface Object {
  toString(): string
  valueOf(): Object
  // ...
}

Object 接口描述的是包括 Object.prototype 在内的所有 Object 实例的公共属性,实例可以通过原型链来访问这些属性。

那么,ObjectObjectConstructor 是什么关系呢?

由于函数是对象的派生实例,不难推断 ObjectConstructorObject 的子类型:

let o1: Object = Object as ObjectConstructor // ✅
let o2: ObjectConstructor = Object as Object // ❌

可以看到,ObjectConstructor 允许赋值给 Object 类型,反之则不行。

03. 类的两种类型

JS 中有且仅有一个 Object 构造函数或 Promise 类,为什么 TS 却需要声明两个接口来描述它们呢?

这是因为 TS 无法通过 直接 实现接口来约束构造函数类型。

假设我们想 DIY 一个 PromiseA 类,且希望它和 ES6 的 Promise 行为一致,我们可能会这样写:

class PromiseA<T> implements Promise<T>, PromiseConstructor {
  then(/** */) {}
  // ❌ all 应该是静态方法,而不是实例方法
  all(/** */) {}
  // ...
}

这里我们好像实现了两个接口,但是很遗憾,PromiseConstructor 接口无法真正约束 PromiseA 类的静态成员,结果导致把 Promise.all() 错误实现为 Promise.prototype.all()

⛔ 注意,implements 子句用于让类实现接口,从而约束了类的 实例成员,但不影响类的静态成员。

如果我们查看 PromiseConstructor 接口的定义,就会发现其中的属性签名没有诸如类静态声明的 static 修饰符:

interface PromiseConstructor {
  // 没有 static 修饰符
  all(/** */): Promise
}

这是因为接口的作用是定义供外部访问的公共数据,所以接口不支持 privatestatic 等限制权限的修饰符。

如果想要约束类的静态成员类型,一种方案是通过类型注解来约束:

const PromiseA: PromiseConstructor = class PromiseA<T> implements Promise<T> {
  then(/** */) {}
  // ✅ all 作为静态方法
  static all(/** */) {}
  // ...
}

在 TS 中声明一个类会引入两种类型:

  • 类类型:实例成员类型
  • 构造函数类型 = 构造签名 + 静态成员类型

03-class.png

class PromiseA {
  // 1. 构造签名:new (cb: Function) => PromiseA
  constructor(cb: Function) {
    /** */
  }
  // 2. 静态成员类型
  static all(iter: any): PromiseA {
    /** */
  }
  // 3. 实例成员类型
  then(f1: Function, f2: Function): PromiseA {
    /** */
  }
}

其中,实例类型与类同名,可以直接复用。

而匿名的构造函数类型则需要显式定义或通过 typeof 查询类型,这在工厂模式中很常见:

// 使用内置的构造函数类型
const ObjectFactory = (Ctor: ObjectConstructor): Object => new Ctor()

// 使用 typeof 获取构造函数类型
typeof PromiseACtor = typeof PromiseA
const PromiseAFactory = (Ctor: PromiseACtor): PromiseA => new Ctor()

在 ES6 之前,JS 没有构造函数,后 ES6 时代的类则可以视为构造函数的 语法糖

如果把 Object() 构造函数当做 class Object {},那么 Object 接口描述的类类型,ObjectConstructor 接口描述的则是构造函数类型。

高潮总结

在 TS 中,Object 既表示 JS 运行时的值,也表示 TS 编译时的类型,可以通过上下文分解来区分。

有趣的是,Object() 构造函数不是 Object 类型,而是 ObjectConstructor 类型。

当我们创造一个类时,会产生与类同名的实例类型和匿名的构造函数类型,类直接实现接口只能约束前者。

我是大家的 林语冰 👨‍💻,欢迎持续 关注,随时了解海内外前端开发的最新情报。

谢谢的大家点赞、留言和友情转发,我们下期再见~

参考文献

[1] typeof 运算符:www.typescriptlang.org/docs/handbo…

[2] ES2025 语言规范:tc39.es/ecma262/mul…

[3] ObjectConstructor:github.com/microsoft/T…

#前端# #javascript# #typescript#