00. Hello World
大家好,我是大家的 林语冰
。
有道是,JS 中万物皆对象,结果最近学 TS 偏偏被对象搞晕了。
在本文中,我们会结合 TS 源码,深度学习 Object
相关的技术细节:
Object
= JS 构造函数 + TS 接口- 为什么
Object
不是Object
类型 - 一个类的两种类型
01. JS 值 vs TS 类型
在 TS 中,一个 Object
表示两种编程概念:
- 一个值:运行时的 JS 构造函数
- 一种类型:编译时的 TS 接口
由于 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
的定义包括三个部分:
- 作为函数:调用签名
- 作为构造函数:构造签名
- 作为对象:其他属性签名
此外,new Object()
返回的 Object
实例类型也是 TS 的内置接口:
// es5.d.ts
interface Object {
toString(): string
valueOf(): Object
// ...
}
Object
接口描述的是包括 Object.prototype
在内的所有 Object
实例的公共属性,实例可以通过原型链来访问这些属性。
那么,Object
和 ObjectConstructor
是什么关系呢?
由于函数是对象的派生实例,不难推断 ObjectConstructor
是 Object
的子类型:
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
}
这是因为接口的作用是定义供外部访问的公共数据,所以接口不支持 private
、static
等限制权限的修饰符。
如果想要约束类的静态成员类型,一种方案是通过类型注解来约束:
const PromiseA: PromiseConstructor = class PromiseA<T> implements Promise<T> {
then(/** */) {}
// ✅ all 作为静态方法
static all(/** */) {}
// ...
}
在 TS 中声明一个类会引入两种类型:
- 类类型:实例成员类型
- 构造函数类型 = 构造签名 + 静态成员类型
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#