零、写在最前
各位看官,由于官方教程对ArkTS的讲解非常概略,且对于TypeScript的基础语法没有任何介绍。这样对于之前没有接触过TypeScript开发的小伙伴造成了不小的困难。
所以,我准备在进行ArkTS语法这小节课程的正式学习之前,先通过其他方式补齐一下TypeScript的基础语法。本文档我会添加我认为重要的补充知识内容,并逐步整理完善,恳请各位看官耐心等待。
当全部更新已完成时,我会在标题上明确写出:(已完结)字样。只要还在更新,就是(正在持续更新中……)。
0.1 TypeScript语法重点总结
0.1.1 数组相关
(1)数组声明的易错点
因为TypeScript的语法规则,允许数组出现联合类型。我们一定要与联合类型的变量的声明区分开。
let x: (number | string) = '123'
而联合类型的数组,其声明的典型错误,就是惯性思维,直接套用了变量的联合类型声明的形式。典型错误如下:
let arr: (number | string) = [1, '123']
正确的声明方式如下:
let arr: Array<number | string>
(2)数组循环遍历的三种方式
TypeScript遍历数组有以下3种方式:for...in、for...of和forEach。前两者更是在书写上极为相似,二者仅仅相差了一个单词,但是彼此是有本质区别的。假设,我们事前已经声明过一个数组:
let arr: Array<number | string> = [1, 2, 3, '1-', '2-', '3-']
所以,我们不妨做如下总结:
- for...in:遍历时,把数组中的每一个下标遍历出来,然后根据数组下标遍历元素。所以,for (const i in arr) {}中的i代表的是arr的元素的下标。
- for...of:遍历时,把数组中的每一个元素的值遍历出来。所以,for (const i of arr) {}中的i代表的是arr中每一个元素的值。
- forEach:
- 遍历时,按“键值对”的形式,一起的从原数组中遍历出来;
- 箭头函数的参数列表中,
- 第一个参数表示的事数组的值;
- 第二个参数表示数组元素的下标。
- 一般情况下,箭头函数的参数列表会同时获取数组元素的键值,如“(v: (number | string), i: number) => {}”。但第二个参数支持省略,如“(v: (number | string)) => {}”;
- 参数间的次序不可以颠倒。
(3)元组相关
a) “此”元组,非“彼”元组
注意!TypeScript中元组的概念和Python中元组的概念有很大的不同!!在Python中元组一经初始化其中的元素就不可修改,而TypeScript中的元组则不同。TypeScript中,元组的概念可以认为是“元素没有类型限制”的数组。所以,TypeScript的元组可以执行增删改查操作。
b) 是数组就不存在越界
TypeScript数组“不存在数组越界”这个说法,这是有别于很多编程语言的。这是我们不妨通过下面这个实例说明这个问题。我们不妨先给出如下TypeScript代码:
// 定义数组arr
let arr: Array<number | string> = [1, 3, 2, '11', '33', '22']
// 定义元组tuple
let tuple = [1, 3, 2, '11', '33', '22']
// 查看两个数组的长度
console.log('arr.length', arr.length);
console.log('tuple.length', tuple.length);
// 访问数组元素,观察返回结果
console.log('arr[10]', arr[10]);
console.log('arr[10]', tuple[10]);
// 查看数组长度是否变化
console.log('arr.length', arr.length);
console.log('tuple.length', tuple.length);
// 查看数组元素是否发生变化
console.log(arr);
console.log(tuple);
// 越界修改数组元素的值
arr[10] = 10
tuple[10] = 10
// 访问数组元素,观察返回结果
console.log('arr[10]', arr[10]);
console.log('arr[10]', tuple[10]);
// 查看数组长度是否变化
console.log('arr.length', arr.length);
console.log('tuple.length', tuple.length);
// 查看数组元素是否发生变化
console.log(arr);
console.log(tuple);
// 访问自动填充数组元素,观察返回结果
console.log('arr[9]', arr[9]);
console.log('arr[9]', tuple[9]);
我们不难发现,在TypeScript中,如果
- 越界访问数组元素,则返回undefined;
- 越界插入数组元素,则数组长度会被延长至理论插入位置,中间元素全部用undefined填充。
c) 它甚至可以给某些变量赋值
我们通过一个例子来解释,请看TypeScript代码:
// 定义元组tuple
let tuple = [1, 3, 2, '11', '33', '22']
// 把元组中的元素展开
let [num1, num2] = tuple
console.log("num1=", num1);
console.log("num2=", num2);
通过这个示例我们可以知道,num1对应元组的第一个元素,num2对应元组的第二个元素。所以,赋值顺序是一一对应的。
(4)Map和Set
二者均为JavaScript ES6版本中添加的一种新的数据结构。
a) 相关操作
Map的相关操作:
- map.clear() - 移除 Map 对象的所有键/值对 。
- map.set() - 设置键值对,返回该 Map 对象。
- map.get() - 返回键对应的值,如果不存在,则返回 undefined。
- map.has() - 返回一个布尔值,用于判断 Map 中是否包含键对应的值。
- map.delete() - 删除 Map 中的元素,删除成功返回 true,失败返回 false。
- map.size - 返回 Map 对象键/值对的数量。
- map.keys() - 返回一个 Iterator 对象, 包含了 Map 对象中每个元素的键 。
- map.values() - 返回一个新的Iterator对象,包含了Map对象中每个元素的值 。
- map.entries() - 返回的
Set的相关操作:
- set.add(value) - 添加某个值,返回 Set 结构本身。
- set.delete(value) - 删除某个值,返回一个布尔值,表示删除是否成功。
- set.has(value) - 返回一个布尔值,表示该值是否为 Set 的成员。
- set.clear():清除所有成员,无返回值。
- set.size - 返回set数据结构的数据长度。
b) 迭代操作
a. Map的迭代操作:
let map = new Map([
['a', 100],
['b', 200],
['c', 300],
])
console.log(map);
a.1 for...of(3种方式)
// 方法1:迭代map中的键
for (const keys of map.keys()) {
console.log(keys);
// 在通过键去找值
console.log(map.get(keys));
}
// 方法2:迭代map中的值
for (const value of map.values()) {
console.log(value);
}
// 方法3:迭代map中的键值对
for (const [k, v] of map.entries()) {
console.log(k, v);
}
a.2 forEach()
map.forEach(
(v, k) => {
console.log(k + " = " + v);
}
)
注意!这里有一个惯性思维容易引起错误的地方。这里箭头函数的参数是“先值,再键”。并不是我常驻口边的“键值对”——“先键,再值”。具体如IDE代码建议所示:
b. Set的迭代操作:
let set = new Set()
set.add(100)
set.add(200)
set.add(300)
b.1 for...of
for (const item of set) {
console.log(item);
}
b.2 forEach()
set.forEach(
(item) => {
console.log(item);
}
)
0.1.2 函数相关
(1)函数参数
TypeScript的函数参数有3种形式:必选参数、可选参数、剩余参数。其中,必选参数与其他编程语言的参数列表基本相同,这里就不介绍了。我们重点看一下后面这两个。
a) 可选参数
- 描述:当参数列表出现可选参数时,允许不对可选参数传参。
- 定义:在参数列表中,形参后:前用?标识,定义为可选参数
function test(str1: string, str2?: string): string {
if (!str2) {
// 当没有对str2传参,会进入这个分支
console.log('str2 is null');
return 'yes';
} else {
// 当对str2传参,会进入这个分支
console.log('str2 is not null');
return 'no';
}
}
let res: string = test('123')
console.log(res);
res = test('123', 'abc')
console.log(res);
b) 剩余参数
- 描述:剩余参数会被当做个数不限的可选参数。可以一个也没有,也可以有任意多个。
- 定义:在参数列表中,用...标识的参数,定义为剩余参数
function sum_up(...arr: number[]): number {
let sum: number = 0
for (const item of arr) {
sum += item
}
return sum
}
let student_score: number[] = [1, 2, 3]
console.log("结果为:", sum_up(...student_score));
console.log("结果为:", sum_up(1, 2, 3, 4, 5));
剩余参数必须遵守的规则:
- 函数中只允许一个剩余参数
- 它必须是数组类型
- 它必须是参数列表的最后一个参数
function sum_up(init_value: number, choice?: number, ...arr: number[]): number {
console.log(`Init value is ${init_value}, choice is ${choice}`);
let sum: number = 0
for (const item of arr) {
sum += item
}
return sum
}
console.log("结果为:", sum_up(1, 2, 3, 4, 5));
let student_score: number[] = [3, 4, 5]
console.log("结果为:", sum_up(1, 2, ...student_score));
(2)匿名函数
ES5规范下,匿名函数在程序运行时动态声明,除了没有函数名外,其他的与标准函数一样。通常情况下,匿名函数必须赋值给一个变量,否则这个函数会因为没有函数名而无法调用。
let sum = function (num1: number, num2: number): number {
return num1 + num2
}
console.log(sum(10, 20));
(3)箭头函数
箭头函数,也称为“函数表达式、Lambda函数”。它是在ES6规范下对匿名函数的简洁写法。因为它相较于ES5规范中的匿名函数省略了function关键字。
let sum = (num1: number, num2: number): number => {
return num1 + num2
}
console.log(sum(10, 20));
(4)函数的重载
定义:函数重载是创建多个方法的机制或能力。这些方法具有相同的名称,具有相同数量的参数,但参数或返回值的类型不同。函数重载也称这个方法重载。
满足函数/方法重载的条件:
- 函数名相同;
- 在每个重载函数中参数的数量不同;
- 参数数量相同时,它们的类型不同;
- 所有重载函数必须具有相同的返回值类型
重载的两个重要部分(声明和实现,二者缺一不可)。下面,我们通过下面这个实例讲解一下
// 函数重载的声明
function add(a: string, b: string): string;
function add(a: number, b: number): number;
// 函数重载的实现
function add(a: any, b: any): any {
return a + b
}
console.log(add('a', 'b'));
console.log(add(1, 2));
使用函数重载的优势
- 它节省空间,使程序执行更快;
- 它提供的代码重用,节省时间和精力;
- 它增加了程序的可读性;
- 它易于代码的维护。
(5)高阶函数
高阶函数和普通函数的不同点在于,它支持接受一个或多个函数作为参数,或者返回的不是一个值而是一个新函数。引入高阶函数的目的是,让函数的处理与使用更加灵活,代码更加简洁和可维护。
【注意】在正式讲解之前,我们要先引入一个概念——“函数的类型”。在TypeScript中,函数的类型是由“参数类型和返回值类型”决定的。
a) 参数为函数
我们可以使用type关键字和箭头函数定义函数类型。
// 目的:定义了一个函数类型
type MyFunc = (a: number) => number;
// 目的:定义一个高阶函数
// 功能:计算两个数字的绝对值之和
function add_up(a: number, b: number, func: MyFunc): number {
return func(a) + func(b)
}
// 目的:定义回调函数
function MyFunc(x: number): number {
if (x >= 0) {
return x
} else {
return -x
}
}
// 调用高阶函数,并输出结果
console.log(`调用高阶函数add_up的返回结果为${add_up(-20, 20, MyFunc)}`);
上述代码中,定义了一个名为MyFunc的函数类型。他接受1个类型为number的参数a,并返回一个number类型的结果。在函数执行的过程中,当调用函数add_up时,传入的第三个函数可以被认为是回调函数。
回调函数是一种在代码中被其他代码调用的函数。在这个例子中,add_up函数接受一个函数作为参数,然后在其内部调用这个函数。这种设计模式允许add_up函数的行为可以根据传入的函数的行为来改变,这使得add_up函数更加灵活和可重用。
此处,我们不妨对于这个问题深入讨论一下。
【问题1】如果我们期望计算两个数的平方和,怎么实现?
这个问题还算简单。就是修改一下回调函数。将回调函数的内容换成返回实参的平方值既可。代码如下:
// 目的:定义了一个函数类型 type MyFunc = (a: number) => number; // 目的:定义一个高阶函数 // 功能:计算两个数字的绝对值之和 function add_up(a: number, b: number, func: MyFunc): number { return func(a) + func(b) } // 目的:定义回调函数 function MyFunc(x: number): number { return x * x } // 调用高阶函数,并输出结果 console.log(`调用高阶函数add_up的返回结果为${add_up(1, -2, MyFunc)}`);
【问题2】如果我们期望实现两个字符串全部转换为小写字母并拼接的操作。那么,有条件去实现吗?
解决这个问题,有两个思路:
{方法1} 利用any类型,使用本篇帖子在“0.1.2/(4)”一节中介绍的“函数的重载”去实现。
// 目的:定义了一个函数类型 type MyFunc = (a: number) => number; // 目的:定义一个高阶函数 // 功能:计算两个数字的绝对值之和 function process(a: any, b: any, func: MyFunc): any { return func(a) + func(b) } // 目的:定义回调函数 function MyFunc(x: any): any { if (typeof x === 'number') { return x * x } else if (typeof x === 'string') { return x.toLowerCase() } } // 调用高阶函数,并输出结果 console.log(`调用高阶函数add_up的返回结果为${process(1, -2, MyFunc)}`); console.log(`调用高阶函数add_up的返回结果为${process('aA', 'BBC', MyFunc)}`);
这里在回调函数中,使用typeof关键字去判断变量的类型。然后,按类型去处理。但是会造成的问题是,因为我们使用了any类型,这就导致了我们失去了在编译阶段对变量类型的检查功能。
那么,有没有办法让我们既能够实现代码的复用,同时又可以保留对于类型的检查功能?这种“既要又要的事情”真的存在吗?答案是存在!但要引入新的知识——泛型。泛型的介绍详见本帖0.1.3节。于是,{方法2}就来了。
{方法2} 使用泛型去解决“既要又要的事情”
// 目的:定义一个高阶函数 // 功能:当传入的内容都是数字,则计算二者之平方和;当传入两个字符串,则将其转化为小写后拼接 function process<T>(a: T, b: T, func: (x: T) => T): T { return func(a) + func(b) } // 目的:定义回调函数 function MyFunc(x: any): any { if (typeof x === 'number') { return x * x } else if (typeof x === 'string') { return x.toLowerCase() } } // 调用高阶函数,并输出结果 console.log(`调用高阶函数add_up的返回结果为${process(1, -2, MyFunc)}`); console.log(`调用高阶函数add_up的返回结果为${process('aA', 'BBC', MyFunc)}`);
这地方需要注意的是,代码的第4行会出现红色警告。警告信息如下:
其实,这个警告信息是可以忽略的。因为,此处的泛型,我们只可能涉及到number和string类型。而这两个类型是支持+运算符的。故可以忽略这个警告。
b) 函数的返回值为函数
通过一个函数去返回一个函数。我们可以使用函数类型来声明函数返回的函数类型。
/** * 这是一个返回求和函数的高阶函数。 * @param {number[]} args - 一个数字数组,用于存储需要求和的数字。 * @return {() => number} - 返回一个函数,该函数无参数,返回值为数组中所有数字的和。 */ function sum_up(...args: number[]): () => number { /** * 这是一个求和函数,用于计算数组中所有数字的和。 * @return {number} - 返回数组中所有数字的和。 */ function inner_sum(): number { // 初始化求和变量 let sum = 0 // 遍历数组,将每个数字加到求和变量上 for (const i of args) { sum += i } // 返回数组中所有数字的和 return sum } // 返回求和函数 return inner_sum } console.log(sum_up(1, 2, 3, 4, 5, 6, 7, 8, 9)());
注意点:
第25行,inner_sum后面没有小括号,是因为inner_sum是一个函数,我们在这里并没有调用它,而是将它返回。第28行,sum_up()后面有一对小括号,是因为我们在这里调用了sum_up函数,并将返回的函数立即执行。
另外,我们看一个更为复杂的案例
我们以下面这段代码为例
function curry<T, U, V>(fn: (a: T, b: U) => V): (a: T) => (b: U) => V { return (a) => (b) => fn(a, b) } function innerAdd(a: number, b: number): number { return a + b } // 功能:调用curry函数,传入innerAdd回调函数,返回一个f。这个f是一个高阶函数。 // f实际上就是(a) => (b) => fn(a, b) let f1 = curry(innerAdd) // 调用f函数,传入参数10,返回一个f2。这个f2却是普通函数! // 为什么呢?实际上,f2就是(b) => fn(a, b)。言下之意,f2的功能就是传入变量b的值,返回的是“调用函数fn(a, b)的返回值”。而函数fn就是我们传入curry函数的回调函数innerAdd。 let f2 = f1(10) // 功能:调用f2函数,传入参数20,打印输出innerAdd函数的返回值 console.log(f2(20));
这段代码的执行逻辑如下:
- 定义一个curry函数,接受一个函数fn作为参数,该函数fn接受两个参数。
- curry函数返回一个新的函数,这个新的函数接受一个参数a,返回一个新的函数,这个新的函数接受一个参数b,调用fn函数,传入参数a和b,返回fn函数的返回值。
- 定义一个innerAdd函数,接受两个参数a和b,返回a和b的和。
- 调用curry函数,传入innerAdd函数,返回一个新的函数f1。
- 调用f1函数,传入参数10,返回一个新的函数f2。
- 调用f2函数,传入参数20,打印输出innerAdd函数的返回值。
0.1.3 泛型相关
泛型(Generics /dʒɪˈnɛrɪk/)是一种编程语言特性,它允许在定义函数、类、接口……时使用占位符来表示类型,而不是具体的类型。
泛型是一种在编写可重用、灵活且类型安全的代码时,非常有用的功能。使用泛型的主要目的事为了处理不特定类型的数据,使代码可以适用于多种数据类型而不失去类型检查。(使用any类型,会失去类型检查)
泛型的优势:
- 代码重用:可以编写与特定类型无关的通用代码,提高代码的复用性;
- 类型安全:在编译时支持进行类型检查,避免在运行时出现类型错误;
- 抽象性:允许编写更抽象和通用的代码,适应于不同类型的数据结构。
(1)泛型标识符
在泛型中,通常使用一些约定俗成的标识符,如T、U、V等。
- T:代表“Type”,是最常见的泛型类型的参数名;
- K, V:用于表示键“Key”和值“Value”的泛型类型参数;
- U, V:用于表示第二、第三个泛型类型参数
function identity<T>(arg:T):T {
return arg
}
但实际上,我们可以使用任意标识符。
0.2 养成查阅文档的好习惯
TypeScript的语法实在是太多,光靠某个老师、某个教程是根本学不完的。所以,遇到不清楚的函数用法,可以分两步走。
0.2.1 在IDE中查询
我使用的事VS Code运行TypeScript代码。在IDE内查询函数的定义和解释的方法,我用下面这个例子来解释。先给出参考代码。
let arr: Array<number | string> = [1, 2, 3, '1-', '2-', '3-']
arr.splice(0,0,3)
console.log(arr);
如果我不清楚splice有什么作用?参数列表中各个参数段什么意思?怎么写?我们可以按ctrl,并将鼠标移动到splice上。点击splice。这样就跳转到了splice的定义了。
0.2.2 从文档中获取
一、课程笔记
(待整理,占位)
二、习题整理
2.1 判断题
(待整理,占位)
答案:
答案:
答案:
2.2 单选题
(待整理,占位)
答案:
答案:
答案:
答案:
2.3 多选题
(待整理,占位)
答案:
答案:
答案:
三、心得体会
3.1 对于ArkTS的学习,需要一定的前驱知识储备
通过对于官方课程的学习,我的感触是它不适合对于TypeScript也是零基础的同学。特别是,当老师讲解到一些术语的时候,根本两眼一抹黑真的不知道。所以,我建议是,有条件稍微过一下TypeScript的语法,有了这个基础再回来听一下ArkTS,可能压力会小很多。
不妨举个例子吧。如下图所示,ArkTS中不支持any类型,并且也不支持在程序运行时,使用delete操作符去修改对象的布局,可TypeScript却支持上述这些操作。
综上,我认为学到这里可以是党暂停一下学习进度,适当参考网上的资源,补充一些稍微涉猎一下TypeScript的基础语法将会有更好的理解。