3. ArkTS语法介绍(正在持续更新中……)

零、写在最前

各位看官,由于官方教程对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));

这段代码的执行逻辑如下:

  1. 定义一个curry函数,接受一个函数fn作为参数,该函数fn接受两个参数。
  2. curry函数返回一个新的函数,这个新的函数接受一个参数a,返回一个新的函数,这个新的函数接受一个参数b,调用fn函数,传入参数a和b,返回fn函数的返回值。
  3. 定义一个innerAdd函数,接受两个参数a和b,返回a和b的和。
  4. 调用curry函数,传入innerAdd函数,返回一个新的函数f1。
  5. 调用f1函数,传入参数10,返回一个新的函数f2。
  6. 调用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的基础语法将会有更好的理解。

  • 9
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值