TypeScript-对象类型
在JavaScript中存在这样一种说法,那就是“一切皆为对象”。有这种说法是因为JavaScript中的绝大多数值都可以使用对象来表示。例如,函数、数组和对象字面量等本质上都是对象。对于原始数据类型,如String类型,JavaScript也提供了相应的构造函数来创建能够表示原始值的对象。例如,下例中使用内置的String构造函数创建了一个表示字符串的对象,示例如下:
const hi = new String('hi');
在某些操作中,原始值还会自动地执行封箱操作,将原始数据类型转换为对象数据类型。例如,在字符串字面量上直接调用内置的“toUpperCase()”方法时,JavaScript会先将字符串字面量转换为对象类型,然后再调用字符串对象上的“toUpperCase()”方法。示例如下:
// 自动封箱,将'hi'转换为String对象类型
'hi'.toUpperCase();
// 自动封箱,将3转换为Number对象类型
// 注意:这里使用了两个点符号,第一个点实际上是一个小数点,只需让JavaScript编译器知道第二个点想要调用属性或方
3..toString()
数组、元组、函数、接口等都属于对象类型,TypeScript提供了多种定义对象的类型,以下介绍下三种基本的对象类型
- Object(首字母大写O)
- object(首字母小写o)
- 对象类型字面量
1. Object
这里的Object指的是Object类型,而不是JavaScript内置的“Object()”构造函数。Object类型表示一种类型,而“Object()”构造函数则表示一个值。因为“Object()”构造函数是一个值,因此它也有自己的类型。但要注意的是,“Object()”构造函数的类型不是Object类型。为了更好地理解Object类型,让我们先了解一下“Object()”构造函数。
JavaScript提供了内置的“Object()”构造函数来创建一个对象。
const obj = new Object();
在实际代码中,使用“Object()”构造函数来创建对象的方式并不常用。在创建对象时,我们通常会选择使用更简洁的对象字面量。虽然不常使用“Object()”构造函数来创建对象,但是“Object()”构造函数提供了一些非常常用的静态方法,例如“Object.assign()”方法和“Object.create()”方法等。
接下来,让我们深入分析一下TypeScript源码中对“Object()”构造函数的类型定义。下面仅摘取一部分着重关注的类型定义(源文件:lib.es5.d.ts):
interface ObjectConstructor {
new(value?: any): Object;
(): any;
(value: any): any;
/** A reference to the prototype for a class of objects. */
readonly prototype: Object;
/**
* Returns the prototype of an object.
* @param o The object that references the prototype.
*/
getPrototypeOf(o: any): any;
/**
* Gets the own property descriptor of the specified object.
* An own property descriptor is one that is defined directly on the object and is not inherited from the object's prototype.
* @param o Object that contains the property.
* @param p Name of the property.
*/
getOwnPropertyDescriptor(o: any, p: PropertyKey): PropertyDescriptor | undefined;
/**
* Returns the names of the own properties of an object. The own properties of an object are those that are defined directly
* on that object, and are not inherited from the object's prototype. The properties of an object include both fields (objects) and functions.
* @param o Object that contains the own properties.
*/
getOwnPropertyNames(o: any): string[];
/**
* Creates an object that has the specified prototype or that has null prototype.
* @param o Object to use as a prototype. May be null.
*/
create(o: object | null): any;
// ....省略了其他成员
}
/**
* Provides functionality common to all JavaScript objects.
*/
declare var Object: ObjectConstructor;
由该定义能够直观地了解到“Object()”构造函数的类型是ObjectConstructor类型而不是Object类型,它们是不同的类型。代码中此行 readonly prototype: Object;
,prototype属性的类型为Object类型。构造函数的prototype属性值决定了实例对象的原型。此外,“Object.prototype”是一个特殊的对象,它是JavaScript中的公共原型对象。也就是说,如果程序中没有刻意地修改一个对象的原型,那么该对象的原型链上就会有“Object.prototype”对象,因此也会继承“Object.prototype”对象上的属性和方法。
现在,我们来说说Object类型。Object类型是特殊对象“Object.prototype”的类型,该类型的主要作用是描述JavaScript中几乎所有对象都共享(通过原型继承)的属性和方法。Object类型的具体定义如下所示(TypeScript源码: lib/lib.es5.d.ts):
interface Object {
/** The initial value of Object.prototype.constructor is the standard built-in Object constructor. */
constructor: Function;
/** Returns a string representation of an object. */
toString(): string;
/** Returns a date converted to a string using the current locale. */
toLocaleString(): string;
/** Returns the primitive value of the specified object. */
valueOf(): Object;
/**
* Determines whether an object has a property with the specified name.
* @param v A property name.
*/
hasOwnProperty(v: PropertyKey): boolean;
/**
* Determines whether an object exists in another object's prototype chain.
* @param v Another object whose prototype chain is to be checked.
*/
isPrototypeOf(v: Object): boolean;
/**
* Determines whether a specified property is enumerable.
* @param v A property name.
*/
propertyIsEnumerable(v: PropertyKey): boolean;
}
通过该类型定义能够了解到,Object类型里定义的方法都是通用的对象方法,如“valueOf()”方法。
- 类型兼容性
Object类型有一个特点,那就是除了undefined值和null值外,其他任何值都可以赋值给Object类型。示例如下:
let obj: Object;
// 正确
obj = { x: 0 };
obj = true;
obj = 'hi';
obj = 1;
// 编译错误
obj = undefined;
obj = null;
对象能够赋值给Object类型是理所当然的,但为什么原始值也同样能够赋值给Object类型呢?实际上,这样设计正是为了遵循JavaScript语言的现有行为。我们在本章开篇处介绍了JavaScript语言中存在自动封箱操作。当在原始值上调用某个方法时,JavaScript会对原始值执行封箱操作,将其转换为对象类型,然后再调用相应方法。Object类型描述了所有对象共享的属性和方法,而JavaScript允许在原始值上直接访问这些方法,因此TypeScript允许将原始值赋值给Object类型。示例如下:
'str'.valueOf();
const str: Object = 'str';
str.valueOf();
注意以下写法是错误的
const point: Object = { x: 0, y: 0 };
此例中,将常量point的类型定义为Object类型。虽然该代码不会产生任何编译错误,但它是一个明显的使用错误。原因刚刚介绍过,Object类型的用途是描述“Object.prototype”对象的类型,即所有对象共享的属性和方法。在描述自定义对象类型时有很多更好的选择,完全不需要使用Object类型,例如接下来要介绍的object类型和对象字面量类型等。。
2. object
在TypeScript 2.2版本中,增加了一个新的object类型表示非原始类型。object类型使用object关键字作为标识,object类型名中的字母全部为小写。示例如下:
const point: object = { x: 0, y: 0 };
object类型的关注点在于类型的分类,它强调一个类型是非原始类型,即对象类型。object类型的关注点不是该对象类型具体包含了哪些属性,例如对象类型是否包含一个名为name的属性,因此,不允许读取和修改object类型上的自定义属性。示例如下:
const obj: object = { foo: 0 };
//: Property 'foo' does not exist on type 'object'.
obj.foo;
在object类型上仅允许访问对象的公共属性和方法,也就是Object类型中定义的属性和方法。示例如下:
const obj: object = { foo: 0 };
obj.valueOf();
只有非原始类型,也就是对象类型能够赋给object类型
let nonPrimitive: object;
// 正确
nonPrimitive = {};
nonPrimitive = { x: 0 };
nonPrimitive = [0];
nonPrimitive = new Date();
nonPrimitive = function () {};
object类型仅能够赋值给以下三种类型:
- 顶端类型any和unknown。
由于所有类型都是顶端类型的子类型,所以object类型能够赋值给顶端类型any和unknown。 - Object类型。
Object类型描述了所有对象都共享的属性和方法,所以很自然地表示对象类型的object类型能够赋值给Object类型 - 空对象类型字面量“{}”
在JavaScript中,有一些内置方法只接受对象作为参数,我们前面提到的“Object.create()”方法,该方法的第一个参数必须传入对象或者null值作为新创建对象的原型。如果传入了原始类型的值,例如数字1,那么将产生运行时的类型错误。
// 正确
const a = Object.create(Object.prototype);
const b = Object.create(null);
// 类型错误
const c = Object.create(1);
3. 对象类型字面量
对象类型字面量的语法与对象字面量的语法相似。在定义对象类型字面量时,需要将类型成员依次列出。对象类型字面量的语法如下所示:
{
TypeMember;
TypeMember;
...
}
常见的属性签名如下(Point对象类型包含两个属性签名类型成员,分别为表示横坐标的属性x和表示纵坐标的属性y,两者的类型均为number类型。):
let point: { x: number; y: number } = { x: 0, y: 0 };
属性签名中的属性名可以为可计算属性名,但需要该可计算属性名满足以下条件之一:
- 可计算属性名的类型为string字面量类型或number字面量类型
const a: 'a' = 'a';
const b: 0 = 0;
let obj: {
[a]: boolean;
[b]: boolean;
['c']: boolean;
[1]: boolean;
};
- 可计算属性名的类型为“unique symbol”类型。
const s: unique symbol = Symbol();
let obj: {
[s]: boolean;
};
- 可计算属性名符合“Symbol.xxx”的形式。
let obj: {
[Symbol.toStringTag]: string;
};
//可选属性
let point: { x: number; y: number; z?: number };
point = { x: 0, y: 0, z: 0 };
//只读属性
let point: {
readonly x: number;
readonly y: number;
};
point = { x: 0, y: 0 };
空对象类型字面量“{}”与Object类型十分相似。而事实上也正是如此,单从行为上来看两者是可以互换使用的。例如,除了undefined值和null值外,其他任何值都可以赋值给空对象类型字面量“{}”和Object类型。同时,空对象类型字面量“{}”和Object类型之间也允许互相赋值。
let a: Object = 'hi';
let b: {} = 'hi';
a = b;
b = a;
两者的区别主要在于语义上。全局的Object类型用于描述对象公共的属性和方法,它相当于一种专用类型,因此程序中不应该将自定义变量、参数等类型直接声明为Object类型。空对象类型字面量“{}”强调的是不包含属性的对象类型,同时也可以作为Object类型的代理来使用。最后,也要注意在某些场景中新的object类型可能是更加合适的选择。
4. 弱类型
弱类型(Weak Type)指的是同时满足以下条件的对象类型:
- 对象类型中至少包含一个属性。
- 对象类型中所有属性都是可选属性。
- 对象类型中不包含字符串索引签名、数值索引签名、调用签名和构造签名
eg
let config: {
url?: string;
async?: boolean;
timeout?: number;
};
5. 多余属性
对象多余属性可简单理解为多出来的属性。eg.
const point: { x: number; y: number } = {
x: 0,
y: 0,
z: 0,
// ~~~~
// z是多余属性
};