二、关联数组/字典的封装
在 JS/TS 中,一切都是对象,函数是对象,数组是对象,object、string 和 number 都是对象,自定义的类型更是对象。
集合表示一组互不相同的元素(不重复的元素)。在字典中,存储的是[键,值]对,其中键名是用来查询特定元素的。
字典和集合很相似,集合以[值,值]的形式存储元素,字典则是以[键,值]的形式来存储元素。
字典也称作映射、符号表或关联数组。
在 TS 语言中,内置了索引签名的功能,该功能可以看成关联数组的经典应用。TS 中声明索引签名的方法:
// TS中声明一个键为string类型,值为string类型的索引签名对象
let strStrDictionary: { [ index : string ] : string };
// TS中声明一个键为number类型,值为string类型的索引签名对象
let numStrDictionary: { [ index: number ]: string };
// 在使用上述两个索引签名对象前,一定要初始化这两个对象,否则报错
strStrDictionray= { };
numStrDictionary= { };
// 接下来就可以使用键值对了
strStrDictionray[ "a" ] ="a";
strStrDictionray[ 'd' ] ="d";
strStrDictionray[ "c" ] ='c';
strStrDictionray[ 'b' ] ="b";
numStrDictionary[ 0 ] ='a';
numStrDictionary[ 3 ] ='d';
numStrDictionary[ 2 ] ='c';
numStrDictionary[ 1 ] ='b';
// 输出结果:{"a":"a", "d":"d", "c":"c", "b":"b"}
console.log( JSON.stringify( strStrDictionray ) );
// 输出结果: {"0":"a", "1":"b", "2":"c", "3":"d"}
console.log( JSON.stringify( numStrDictionary ) );
注意:在 TS 索引签名中,作为键的数据类型只能是 string 或 number 类型(如上代码所示),而对应的值可以是任意类型。
在 ES6(ES 2015)规范中新提供了一个关联数组结构,即 Map 对象。
1、Object、索引签名、ES6 Map 对象之间的区别
在 JS/TS 中,可以直接用 object 对象,通过字符串的键查找到对应的值,但是如果使用 object 的话,则键只能使用 string 类型。
TS 中的索引签名只能使用 string 及 number 类型作为键的数据类型。
ES6 规范中的 Map 对象的键的数据类型可以是 JS/TS 中的任意类型。例如键可以是 string、number 等基本数据类型,也可以是 object、String、Number,以及各种自定义的引用类型。
ES6 中的 WeakMap 对象的键只能使用引用类型。例如,string、number 等基本数据类型不能作为键的类型。
如果要在 TS 中使用 Map 对象,则 tsconfig.json 中的 target 项的值需要设置为 ES 2015 及以上。
2、索引签名
索引:对象或数组的对应位置的名字
数组的索引就是 number 类型的 0,1,2,3...对象的索引就是 string 类型的属性名
数字索引签名:通过定义接口用来约束数组
type numberIndex{
[index:number]:string
}
consttestArray:numberIndex= ["1","2",3]// 不能将类型“number”分配给类型“string”。ts(2322) 所需类型来自此索引签名
可以看到 testArray 数组的第三位不符合 numberIndex 的约束.
索引签名的名称如[index:number]:string里的index除了可读性外,并无任何意义.但有利于下一个开发者理解你的代码.
字符串索引签名:用于约束对象
type objectType{
[propName:string]:number
}
consttestObj:objectType= {
"name":100,
"age":"200"// 不能将类型“string”分配给类型“number”。ts(2322) 所需类型来自此索引签名。
}
可以看到 testObj 的第二个对象不符合 objectType 的约束.
注意事项
可以看到上述的例子我都没有在类型别名中添加其他的约束条件,仅写了一个索引签名约束
type attentionType{
name: string; // Ok
age?: number; // 类型“number | undefined”的属性“age”不能赋给“string”索引类型“string”。ts(2411)
sex?: undefined; // OK
[propName: string]: string|undefined;
}
上述例子说明了,一旦定义了索引签名,那么确定属性和可选属性的类型都必须是它的类型的子集
可以同时使用两种类型的索引,但是数字索引的返回值必须是字符串索引返回值类型的子类型。 这是因为当使用 number 来索引时,JavaScript 会将它转换成 string 然后再去索引对象。
interface Animal {
name: string;
}
interface Dog extends Animal {
breed: string;
}
interface NotOkay {
[x: string]: Dog;
[x: number]: Animal; // Error
}
interface Okay {
[x: string]: Animal;
[x: number]: Dog; // OK
}
3、关联数组底层实现的方法
一般而言,关联数组基本都是使用红黑树(Red-Black Tree)或 哈希表(Hash Table)这两个数据结构实现的。
在 JS 语言中:
// TS中声明一个键为string类型,值为string类型的索引签名对象
let strStrDictionary: { [ index : string ] : string };
// 在使用上述索引签名对象前,一定要初始化这两个对象,否则报错
strStrDictionray = { };
// 接下来就可以使用键值对了
strStrDictionray[ "a" ] = "a";
strStrDictionray[ 'd' ] = "d";
strStrDictionray[ "c" ] = 'c';
strStrDictionray[ 'b' ] = "b";
// 输出结果:{"a":"a", "d":"d", "c":"c", "b":"b"}
console.log( JSON.stringify( strStrDictionray ) );
可以看到打印结果为 a、d、c、b,而不是(a、b、c、d)
在 C++ 语言中:
看一下C++ STL(标准模板库)中基于红黑树和哈希表实现的关联数组之间的区别与联系
using namespace std;
typedef map<string, string> StringMap;
typedef StringMap::iterator StringMapIter;
typedef unordered_map<string, string> StringHashMap;
typedef StringHashMap::iterator StringHashMapIter;
int main()
{
printf("使用红黑树\n");
StringMap strmap;
// 乱序插入字母
strmap["a"] = "a";
strmap["d"] = "d";
strmap["c"] = "c";
strmap["b"] = "b";
// 使用迭代器遍历所有key
StringMapIter mapIter = strmap.begin();
while (mapIter ! = strmap.end()) {
printf("key = %s\n", mapIter->first.c_str());
++mapIter;
}
printf("使用哈希表\n");
StringHashMap hashmap;
hashmap["a"] = "a";
hashmap["d"] = "d";
hashmap["c"] = "c";
hashmap["b"] = "b";
StringHashMapIter hashIter = hashmap.begin();
while (hashIter ! = hashmap.end()) {
printf("key = %s\n", hashIter->first.c_str());
++hashIter;
}
getchar();
return 0;
}
通过上述两个Demo,我们可以清晰地知道如何区分关联数组底层所使用的数据结构:
如果关联数组的键输出顺序自动排序,则肯定是红黑树实现;
如果关联数组的键输出与插入时保持一致,则肯定是哈希表实现。
4、Dictionary 字典对象的封装实现
新建了 Dictionary.ts 文件,文件内容如下:
/*
* @Description:
* @Author: tianyw
* @Date: 2023-01-27 11:03:44
* @LastEditTime: 2023-01-27 11:20:23
* @LastEditors: tianyw
*/
export class Dictionary<T> {
// 内部封装了索引签名或 ES6 Map 对象,其键的数据类型为 string,泛型参数可以是任意类型
// 强制键的类型必须为 string 类型
private _items: { [k: string]: T } | Map<string, T>;
// 用来跟踪目前的元素个数,在成功调用 insert 方法后递增,在 remove 方法后递减
private _count: number = 0;
// 默认使用 ES6 Map 对象来管理相关数据
// 构造函数 根据参数 uesES6Map 决定内部使用哪个关联数组
public constructor(useES6Map: boolean = true) {
if (useES6Map === true) {
this._items = new Map<string, T>();
} else {
// 如果不使用 Map,则选择使用索引签名类型来管理相关数据
this._items = {}; // 初始化索引签名
}
}
// 只读属性:获取字典的元素个数
public get length(): number {
return this._count;
}
// 判断某个键是否存在
public contains(key: string): boolean {
if (this._items instanceof Map) {
return this._items.has(key);
} else {
return this._items[key] !== undefined;
}
}
// 给定一个键,返回对应的值对象
public find(key: string): T | undefined {
if (this._items instanceof Map) {
return this._items.get(key);
} else {
return this._items[key];
}
}
// 插入一个键值对
public insert(key: string, value: T): void {
if (this._items instanceof Map) {
this._items.set(key, value);
} else {
this._items[key] = value;
}
this._count++;
}
// 删除
public remove(key: string): boolean {
let ret: T | undefined = this.find(key);
if (ret === undefined) {
return false;
}
if (this._items instanceof Map) {
this._items.delete(key);
} else {
delete this._items[key];
}
this._count--;
return true;
}
// 获取所有键
public get keys(): string[] {
let keys: string[] = [];
if (this._items instanceof Map) {
let keyArray = this._items.keys();
// 返回的是 IterableIterator<T> 类型,该类型能别用于 for...of... 语句,由于设计的 Dictionary 对象的 keys 属性返回的是数组对象 因此需要再次包装一下
for (let key of keyArray) {
keys.push(key);
}
} else {
for (let prop in this._items) {
if (this._items.hasOwnProperty(prop)) {
keys.push(prop);
}
}
}
return keys;
}
// 获取所有值
public get values(): T[] {
let values: T[] = [];
if (this._items instanceof Map) {
// 一定要用of,否则出错
// 返回的是 IterableIterator<T> 类型,该类型能别用于 for...of... 语句,由于设计的 Dictionary 对象的 values 属性返回的是数组对象 因此需要再次包装一下
let vArray = this._items.values();
for (let value of vArray) {
values.push(value);
}
} else {
for (let prop in this._items) {
if (this._items.hasOwnProperty(prop)) {
values.push(this._items[prop]);
}
}
}
return values;
}
}
使用 demo,方法如下:
DSDictionary.ts 文件内容:
/*
* @Description:
* @Author: tianyw
* @Date: 2023-01-26 18:49:30
* @LastEditTime: 2023-01-27 14:22:41
* @LastEditors: tianyw
*/
import { Dictionary } from "../../src/dataStructures/Dictionary";
function run() {
let dict: Dictionary<string> = new Dictionary(false);
dict.insert("a", "a");
dict.insert("d", "d");
dict.insert("c", "c");
dict.insert("b", "b");
console.log(JSON.stringify(dict));
dict.remove("c");
console.log(JSON.stringify(dict));
console.log(dict.contains("c"));
console.log(dict.find("b"));
console.log(JSON.stringify(dict.keys));
console.log(JSON.stringify(dict.values));
}
run();
本章参考如下:
《TypeScript 图形渲染实战——基于WebGL的3D架构与实现》