第3章 TypeScript基础 【学习目标】  了解TypeScript语言的特点  认识TypeScript语言支持的数据类型  会用TypeScript语言的控制结构、函数  理解TypeScript语言的面向对象特征并能够正确使用 本章介绍TypeScript基础,由于华为公司推出的ArkTS是基于TypeScript语言的,因此要求读者具有一定的语言基础,已经熟悉TypeScript的读者可以跳过本章。另外,本章对TypeScript的介绍是简明扼要的,是以在HarmonyOS应用开发中可以使用TypeScript语言为目的,如果读者需要进一步掌握TypeScript,则需要学习其他专门针对该编程语言的资料。 3.1TypeScript语言简介 TypeScript是由微软开发的自由和开源的编程语言,它是JavaScript的一个超集。JavaScript从1995年问世,现在已成为最广泛使用的跨平台语言之一。虽然采用JavaScript 编写的程序的大小、范围和复杂性呈指数级增长,但JavaScript语言表达严重不足,同时编码过程中的常见拼写、类型等错误都不能预先检查。TypeScript的最初目标是成为JavaScript程序的静态类型检查器,但随着其发展,TypeScript的目标更多是开发大型应用,同时其代码可以编译生成纯JavaScript代码,并可运行在任何浏览器上。 TypeScript语言为JavaScript增加了新的特性,主要包括以下几种新特性。  类型批注和编译时类型检查  类型推断和类型擦除  枚举  元组  接口  泛型编程  名字空间  类  模块  Lambda函数  可选参数及默认参数等 TypeScript包含了JavaScript的全部特性,支持ES6标准,ES6的全称是ECMAScript 6.0。由于JavaScript是被Oracle公司注册的商标,因此,JavaScript的正式名称是ECMAScript。1996年11月,JavaScript的创造者网景公司将其提交给国际化标准组织欧洲计算机制造联合会(European Computer Manufactures Association,ECMA),希望其能够成为国际标准,后来便有了ECMAScript。2009年12月,ECMAScript 5.0 版正式发布,简称ES5。2015年6月,ES6正式成为国际标准。图31表示了TypeScript、JavaScript、ES5、ES6之间的关系。 图31TypeScript、JavaScript、ES5、ES6关系 3.2TypeScript简单使用 使用TypeScript编写程序,需要安装其编译环境,安装编译环境首先需要安装NPM工具,NPM的全称是Node Package Manager,是一个Node.js包管理和分发工具,已经成为Node模块的标准。NPM是可以随同Node.js一起安装的包管理工具,因此可以直接安装Node.js以安装NPM。关于Node.js的安装可以访问其官网下载并安装。 假设本地环境已经安装了NPM工具,可以使用以下命令来安装TypeScript。首先通过命令设置镜像网址,命令如下: npm config set registry https://registry.npmmirror.com 通过命令安装TypeScript,命令如下: npm install -g typescript 待安装完成后,可以使用tsc v命令查看版本信息,如果显示出版本信息,则说明TypeScript编译器已经安装成功,显示版本信息的命令如下: tsc -v Version 4.7.4 在当前目录下,建立一个文本文件,并输入下面的内容,将文件名保存为app.ts。TypeScript源程序的扩展名为ts,代码如下: var msg:string = "Hello World" console.log(msg) 这样,一个最简单的TypeScript程序就写好了,然后可以执行以下命令将TypeScript代码转化成JavaScript代码如下: tsc app.ts 此时,会在当前目录下生成一个app.js文件,其内容如下: var msg = "Hello World"; console.log(msg); 接下来,可以使用node命令来执行JavaScript代码,执行app.js文件的命令如下: node app.js 上述由app.ts转换为app.js的过程表明TypeScript代码是通过TypeScript编译器(tsc)把代码转化成了JavaScript代码,如图32所示,最终真正运行的还是JavaScript程序。 图32TypeScript编译成JavaScript TypeScript会忽略程序中出现的空格、制表符和换行符。空格、制表符通常用来缩进代码,使代码易于阅读和理解。 TypeScript区分大写和小写字符,即仅大小写不同的标识符也被认为是不同的标识符。 TypeScript语句后面的分号(;)是可选的,当一行只有一个语句时,其后可以使用分号,也可以不使用,如果一行有多个语句,则需要使用分号分隔,代码如下: console.log("Hello"); console.log("World"); TypeScript支持两种类型的注释,单行注释采用“//”,一行中在//后面的文字是注释内容,多行注释采用“/*”“*/”,位于二者之间的多行内容为注释。这一点和很多编程语言相同。 3.3基本类型和运算符 3.3.1数据类型 TypeScript是强类型语言,要求所有的量都要有明确的类型,TypeScript语言支持的类型包括数值类型、字符串类型、布尔类型、数组类型、元组类型、枚举类型、void类型、null类型、undefined类型、never类型和任意类型。 数值类型: 数值类型用于表示数值,其值为64位浮点值,可以表示整数和浮点数,可以采用二进制、八进制、十进制和十六进制。数值类型对应的关键字是number。下面是数值类型变量的示例,代码如下: let x: number = 100//本质不是整数,是浮点数 let y: number = 3.14 //浮点数 let a: number = 0b111 //二进制 let b: number = 0o76 //八进制 let c: number = 60 //十进制 let d: number = 0xff0000 //十六进制 字符串类型: 若干个字符组成的串,可以使用单引号('),也可以使用双引号(")引起来,单引号和双引号要成对出现,如果字符串中间需要有引号,则可以使用转义字符。字符串类型对应的关键字是string。下面是字符串类型变量的示例,代码如下: let s1: string = '张三' //单引号 let s2: string = "李四" //双引号 let s3: string = "I'm Wang Wu" //双引号内含有单引号,可以不转义 let s4: string = "I\'m Zhao Liu" //双引号内含有单引号,也可以转义 let s5: string = 'I\'m Sun Qian' //单引号内含有单引号,必须转义 当字符串中需要出现特殊字符时,需要用转义字符,常用的转义字符见表31。 表31常用的转义字符 代码输出代码输出 \'单引号\r回车符 \"双引号\t制表符 \\反斜杠\b退格 \n换行符\f换页符 另外,字符串还可以使用反引号(`)来定义多行文本和内嵌表达式,内嵌表达式采用${变量}形式表示。下面是反引号的示例,代码如下: let name: string = "ZhangSan" let age: number = 18 let msg: string = `基本信息,姓名: ${ name } 年龄: ${ age }` console.log( `
${msg}
` ) 布尔类型: 表示逻辑值,对应的关键字是boolean,布尔类型只有两个值,分别为true和false。例如定义布尔变量的代码如下: let r: boolean = true 数组类型: 若干个相同的类型组成的一组数据,在一种类型后面加上中括号([])即表示该类型对应的数组类型。另外,数组还可以使用泛型。例如定义数组的代码如下: let arr1: number[] = [1,2,3,4,5] let arr2: Array = [1,2,3,4,5,6] 元组类型: 元组可以理解成已知元素数量和类型的特殊数组,和数组不同的是,元组中各元素的类型不必相同。元组类型使用的示例代码如下: let t: [string,number] //t为二元元组 t = [ "price",10.6 ] //赋值 console.log( t[1] ) //输出10.6 t = [ 10.6,"price" ] //错误 枚举类型: 枚举类型用于定义若干个数值集合,使用的关键字是enum。枚举类型使用的示例代码如下: enum Color { Red, Green, Blue } let c: Color = Color.Blue console.log(c); //输出2 函数空类型: 即void类型,用于表示函数返回值的类型,当函数不需要有返回值时,可以将其返回类型表示为该类型。函数空类型使用的示例代码如下: function printinfo(): void { alert("This is something"); } 空类型: 也称为null类型,表示对象值缺失,在JavaScript中null表示空,即什么都没有,null也是一个特殊值,表示一个空对象引用。当使用typeof判断null时,其返回的是object。 未定义类型: 即undefined类型,表示未定义,在JavaScript中,当一个变量没有设置值时就为undefined。通过typeof判断一个没有值的变量会返回undefined。 null和undefined是其他任何类型(包括void)的子类型,可以赋值给其他类型,赋值后的类型会变成null或undefined,但是在TypeScript中启用严格的空校验(strictNullChecks)特性,就可以使null和undefined只能被赋值给void或本身对应的类型,示例代码如下: //启用 --strictNullChecks let x: string; x = "good"; //编译正确 x = undefined; //编译错误 x = null; //编译错误 在上面的例子中变量x只能是数字类型。如果一种类型可能出现null或undefined,则可以用 | 来支持多种类型,示例代码如下: //启用 --strictNullChecks let y: number | null | undefined; y = 1; //编译正确 y = undefined; //编译正确 y = null; //编译正确 无果类型: 即never类型,表示从不会出现的值,声明为never类型的变量只能被never类型所赋值,never在函数中通常表现为抛出异常或无法执行到终止点,如无限循环。never类型是其他类型的子类型,包括null和undefined,示例代码如下: let a: never let b: number a = 100 //编译错误,数值不能赋值给never类型 a = (()=>{ throw new Error('error')})() //正确,never类型赋值 b = (()=>{ throw new Error('error')})() //正确,never赋值给数值类型 function error(msg: string): never { //函数返回never类型 throw new Error(msg); } function loop(): never { //函数返回never类型,可以理解成永远不返回 while (true) {} } 任意类型: 也称为any类型,是针对类型不明确的变量使用的一种数据类型,常用于类型会动态改变的情况,示例代码如下: let x: any = 1 //数字类型 x = 'I am zhangsan' //字符串类型 x = false //布尔类型 x.show() //当x是一个对象时,show方法在运行时可能存在,编译时不检查 let arr: any[] = [ 0, true, 'good' ]//any类型数组 arr[1] = 100 总体来讲,TypeScript语言支持的数据类型还是比较丰富的。除了支持基本的类型外,TypeScript还支持自定义类型,如类等。 数据类型在使用的过程中一般遵循一致性原则,即什么类型的变量就赋予对应类型的值。 通过类型定义变量的一般格式如下: 修饰符变量名:类型名 [= 值] TypeScript变量的使用规则如下: (1) 变量名可以包含数字、字母、下画线(_)和美元($)符号,不能包含其他特殊字符和空格。 (2) 变量名不能以数字开头。 (3) 变量使用前必须先声明。 (4) 变量类型可以自动推定和隐式转换。 变量的使用,示例代码如下: var s1:string = "good"//定义变量并初始化 var s2:string//未初始化,变量值会被设置为 undefined var s3 = "anything"//自动将s3推定为any类型 var s4//s4为any类型,值为 undefined console.log(typeof(s4))//输出undefined,因为s4未赋值 s4 = "anything"//var修饰表示变量,可以多次被赋值 console.log(typeof(s4))//输出string let x: number = 100 console.log(typeof(x))//输出number x = s4//any类型可以赋值给number,自动转换 console.log(typeof(x))//输出string x = s1//错误,字符串不能赋值给数值实例 在TypeScript中,可以通过管道符号( | )将变量定义成多种类型,示例代码如下: var val:string | number//val可以是string或number类型 val = 6 val = "some" console.log("val=" + val)//最后一次赋值的结果 3.3.2运算符 TypeScript运算符包括算术运算符、关系运算符、逻辑运算符、位运算符、赋值运算符、三元条件运算符、类型运算符、其他运算符。 算术运算符包括加(+)、减(-)、乘(*)、除(/)、求余(%)、自增(++)和自减(--)。 关系运算符包括等于(==)、不等于(!=)、大于(>)、小于(<)、大于或等于(>=)、小于或等于(<=)。另外还有强等于(===),用于判断值和类型是否同时相同,强不等于(!==)会要求值和类型都不相同。 逻辑运算符包括逻辑与(&&)、逻辑或(||)、逻辑非(!)。 位运算符包括位与(&)、位或(|)、取反(~)、异或(^)、左移(<<)、右移(>>)、无符号右移(>>>)。 赋值运算符包括赋值(=)、复合加赋值(+=)、复合减赋值(-=)、复合乘赋值(*=)、复合除赋值(/=)。 三元条件运算符只有问号冒号运算符(?:)。 类型运算符包括typeof和instanceof。typeof是一元运算符,返回的是操作数的数据类型。instanceof运算符用于判断对象是否为指定的类型的实例。 除了上述的运算符外,还有一些其他运算符。如负号运算符(-),其写法和减号运算符一样,作为一元运算符时表示负号; 字符串连接运算符(+),其写法和加法运算符一样,当有字符串参与时表示连接; 分量运算符(.),用于类或对象引用其分量; 下标运算符( [ ] ),用于数组或元组引用其分量。 TypeScript运算符的使用和很多其他高级编程语言中的运算符类似,这里限于篇幅不进行详细说明。 3.4控制语句和函数 3.4.1控制语句 程序的基本控制结构有顺序结构、分支结构和循环结构,这一点在所有的高级编程语言中都是适用的。 顺序结构比较简单,程序按照语句的先后次序执行,不需要控制语句进行控制。 1. 分支语句 分支结构也称为选择结构,分支语句根据不同的条件来执行不同的分支,TypeScript分支语句有if语句、if…else语句、if…else if…else语句、switch…case语句。 if语句由一个布尔表达式后跟一个或多个语句组成,语法格式如下: if( 布尔表达式 ){ //布尔表达式true执行语句块 } if…else 语句有两个分支,if后跟一个,else后跟一个,语法格式如下: if( 布尔表达式 ){ //在布尔表达式为 true 时执行 }else{ //在布尔表达式为 false 时执行 } if…else if…else语句相对于在if…else语句中嵌套了一个if…else语句,在执行多个判断条件时比较有用,语法格式如下: if( 布尔表达式1 ) { //在布尔表达式1 为 true 时执行 } else if( 布尔表达式2 ) { //在布尔表达式2 为 true 时执行 } else if( 布尔表达式3 ) { //在布尔表达式3 为 true 时执行 } else { //在前面布尔表达式都为 false 时执行 } switch…case语句是一个多路分支语句,一个switch语句允许测试一个变量等于多个值的情况,每个值称为一个case,语法格式如下: switch( 表达式 ){ case 常量表达式1 : //执行语句 break;//可选执行break,跳出switch语句 case 常量表达式2 : //执行语句 break;//可选如果没有break,则继续向下执行 //可以有任意多个case default : // 可选的 // //默认执行语句 } 使用switch语句必须遵循以下规则: (1) 在一个switch中可以有任意数量的case语句,每个 case 后跟一个要比较的值和一个冒号。 (2) case后的常量表达式必须和switch后括号内的表达式具有相同的数据类型,并且必须是一个常量或字面量。 (3) 当被判断的表达式值等于case中的常量时,case后跟的语句将被执行,直到遇到break语句为止。 (4) 当遇到break语句时,switch终止,控制流将跳转到switch语句后。 (5) 不是每个case都需要包含break。如果case语句不包含break,则控制流将会继续判断后续的case是否为真,直到遇到break为止。 (6) 一个switch语句可有一个可选的default,出现在switch结尾。default可用于在上面所有case都不为真时执行一个任务,default不是必需的。 2. 循环语句 循环结构是编程过程中常用的结构,TypeScript语言中循环语句有for语句、for…in语句、while语句、do…while语句,另外还支持for…of、for…in、forEach、every和some循环。 for语句的语法格式如下: for( 表达式1 ; 表达式2 ; 表达式3 ){ //循环体代码 } for循环语句的控制流程如下: (1) 首先执行表达式1,并且只执行一次。表达式1一般为初始化表达式,表达式1可以为空。 (2) 接着会判断表达式2,如果表达式2的结果为true,则执行循环体。如果值为false,则循环终止。表达式2是循环条件表达式。 (3) 每执行一遍循环体后,控制流会跳到表达式3。表达式3一般为增量表达式,表达式3也可以为空。 (4) 再次执行表达式2,如果表达式2为true,则继续循环,这个过程会不断重复,直到表达式2为false 时,循环终止。 while循环也称为当型循环,while语句语法格式如下: while( 循环条件表达式 ){ //循环体 } 在while语句中,当循环条件表达式为true时,执行循环体,否则终止循环。 do…while循环也称为直到型循环,do…while语句首先执行一遍循环体,在循环的尾部检查循环条件表达式,其语法格式如下: do{ //循环体 }while( 循环条件表达式 ); 在do…while语句中,同样是当循环条件表达式为true时,继续执行循环体,否则终止循环。 for…of循环语句是在ES6中引入的,以替代for…in和forEach()。for…of语句允许遍历Arrays(数组)、Strings(字符串)、Maps(映射)、Sets(集合)等可迭代的数据结构,示例代码如下: let arr = [ 6, "some", false ] for(let e of arr) { console.log( e ); //依次输出6、some和false } for…in和for…of都可以遍历可迭代的数据结构,不同的是,for…in返回的是被迭代对象的键,而for…of返回的是被迭代对象的属性的值,示例代码如下: let arr = [ 6, "some", false ] for(let e in arr) { console.log( e ); //依次输出0,1和2 } forEach、every和some是JavaScript的循环语法,TypeScript作为JavaScript的语法超集,当然默认也是支持的。这3个循环更像是可以迭代对象的方法。 forEach的基本用法,示例代码如下: const arr = ['a', 'b', 'c']; arr.forEach( element => console.log(element) );//对每个元素执行操作 arr.forEach( ( value, index, array ) => { //又一种用法 //value代表当前值 //index代表当前下标 //array代表数组本身 } ); 因为forEach在迭代器中是无法返回的,所以可以使用every和some来取代forEach。every循环的代码如下: let list = [100, 200, 300]; list.every((value, index, array) => { //value代表当前值 //index代表当前下标 //array代表数组本身 return r;//当返回值为true时继续,当返回值为false时退出循环 //在循环过程中一旦执行返回值为false循环就停止 } ); some循环的代码如下: let list = [100, 200, 300]; list.some((value, index, array) => { //value代表当前值 //index代表当前下标 //array代表数组本身 return r; //在循环过程中一旦执行返回值为true循环停止 } ) every()方法使用指定函数检测数组中的所有元素,如果数组中检测到有一个元素不满足(以返回值为false判定),则剩余的元素不会再进行检测,every()的返回值也为false。如果所有元素都满足条件,则every()的最终返回值为true。 some()方法依次执行数组的每个元素,如果有一个元素满足条件(以返回值为true判断),则剩余的元素不会再执行检测,some()的返回值为true。如果没有一个满足条件的元素,则some()的最终返回值为false。 3. 跳转语句 在使用循环的过程中,可以使用break和continue语句进行跳转。 当在循环体内执行到break语句时,循环会立即终止,并且程序流将继续执行紧接着循环的下一条语句。如果循环有嵌套关系,则break语句停止的是其所在的当前层循环,语法格式如下: break;//分号可以省略 当在循环体内执行到continue语句时,它不是终止循环,而是会跳过当前循环中的剩余代码,执行当前循环的下一轮循环。对于for循环,continue语句执行后,会跳转到其第3个表达式。对于while和do…while语句,continue语句执行后,会跳转到执行循环判断条件,其语法格式如下: continue;//分号可以省略 3.4.2函数 1. 一般函数 函数是若干语句组成的功能块,函数是组织程序的有效方法。函数声明需要让编译器能够辨析函数名、参数和返回类型。函数体是函数的执行功能代码块。TypeScript语言中定义函数的基本语法格式如下: function函数名( 参数:类型 ):返回类型 {//function为关键字 //函数体代码 } 调用函数的基本语法格式如下: 函数名( 实际参数 ) 定义函数时,函数名必须符合标识符规则。 在函数内返回,可以执行return语句,return返回的类型应该和函数的返回类型一致。 函数定义时,可以包含多个参数,多个参数之间以逗号分隔。当函数有多个参数时,调用时也应输入相关个数的实际参数,实际参数会对应传递给函数的参数。 函数定义时,可以定义可选参数,可选参数调用时可以不传递实际参数,可选参数比必须参数在函数定义形式上多一个问号(?)。定义可选参数的函数的基本语法格式如下: function函数名( 参数1: 类型1, 可选参数?: 类型 ) { //函数体代码 } 在有可选参数的情况下,可选参数必须跟在必须参数后面。 函数定义时,可以设置参数的默认值,这样在调用函数时,如果不传入该参数的值,则使用默认的参数值,定义带默认参数的函数的基本语法格式如下: function 函数名( 参数1:类型 , 参数2:类型 = 默认值) { //函数体代码 } 默认值参数,在函数调用时也可以传递参数,此时会使用传递的参数值,也可以不传递参数,此时会使用默认值。默认值参数也必须跟在必须参数的后面。 在定义函数时,参数不能同时设置为可选参数和默认值参数。 在定义函数时,还可以定义剩余参数,剩余参数针对函数参数个数不确定的情况,剩余参数语法允许将一个不确定数量的参数作为一个数组输入,剩余参数的声明名称前有3个点( ... ),示例代码如下: function getSum( ...nums:number[] ) {//带有剩余参数 var i; var sum:number = 0; for( i = 0;i { //返回类型为string return "x="+x } console.log( f(6) ) 在使用Lambda表达形式时,其箭头(=>)前相当于函数头说明,箭头(=>)后面相当于函数体。 3.5类和接口 3.5.1类和对象 TypeScript语言可以说是面向对象的JavaScript。TypeScript支持面向对象的基本特性,具有类、对象、接口、继承等语言表达。 1. 类的定义 类是描述了所创建的对象的共同的属性和方法的抽象,在TypeScript中,类定义的基本格式如下: class 类名 { //类体 } 定义类的关键字为class,后面紧跟类名,然后是花括号括起来的类体。类体中可以包含属性和方法,示例代码如下: class Person { name:string;//属性 constructor(name:string) {//构造函数,有特定的名字,不和类同名 this.name = name } show():void {//方法,注意前面没有function关键字 console.log("姓名:"+ this.name ) } } 2. 创建使用对象 对象是类的实例,使用new关键字创建对象,语法格式如下: var 对象名 = new类名( 参数 )//参数 通过类创建对象时,会调用相应的构造函数,示例代码如下: var obj = new Person("张三") 类中的属性和方法可以使用点运算符(.)访问,示例代码如下: obj.name = "李四"//访问属性 obj.show()//访问方法 对象其实就是包含一组键值对的实例,这里的值可以是标量、函数、数组、对象等。在TypeScript中,除了可以通过类创建对象外,还可以直接使用类似JSON格式的方式创建对象,示例代码如下: var obj = { name: "王五",//标量 say: function() { //函数 console.log( this.name ) }, scores:[100,99]//集合 } obj.say()//调用say函数 3. 成员权限 类中的成员有公有(public)、私有(private)与保护(protected)3种保护权限,在默认情况下为public权限,成员权限说明的示例代码如下: class OtherPerson { privatename:string;//私有属性 protectedid:string;//保护属性 public show():void {//公有方法,public可以省略 console.log("姓名:"+ this.name ) } } 具有公有权限的成员在类外和类内都能访问,具有私有权限的成员只能在类内访问,具有保护权限的成员可以在类内和子类中访问。 4. static关键字 关键字static用于将类的成员(属性和方法)定义为静态的,静态成员属于类本身,可以直接通过类名调用,示例代码如下: class Car { static num:number;//静态成员 public static disp():void {//public可以省略 console.log("num 值为 "+ Car.num) } } Car.num = 12 //初始化静态变量 Car.disp()//调用静态方法 5. 类的继承 TypeScript中类的继承使用关键字extends,在TypeScript中只支持单继承,即一个类最多只能有一个父类。继承可以重写方法,可以通过super访问父类中的成员,示例代码如下: class Studentextends Person { major:string//增加了新属性 constructor( n:string,m:string ) {//构造函数 super(n)//调用父类构造函数 this.major = m } show():void {//重写方法 super.show()//调用父类中的函数 console.log( "专业:"+this.major) } } var obj = new Student("张三","计算机") obj.show() 3.5.2接口 接口是一系列抽象的声明,TypeScript定义接口的基本语法如下: interface 接口名 {} 接口在TypeScript中使用非常灵活,它可以作为类型限制数据,示例代码如下: interface LabelType { tag: string } function printLabel( label: LabelType) { //这里参数label要符合LabelType的接口规则 //参数必须包含一个tag属性 console.log( label.tag ) } let obj = { api:9 , tag: "HarmonyOS" } printLabel( obj )//输出 HarmonyOS 接口可以继承,也就是说接口可以扩展于其他接口,TypeScript中允许接口继承一个或多个接口,继承关键字是extends,多继承时父接口以逗号分隔,接口继承的语法格式如下: interfacesub_name extends super_name1 [, super_name2 ] 关于继承的基本使用,示例代码如下: interface I1 { a:number } interface I2 { b:number } interface I extends I1, I2 { } var Iobj:I= { a:100, b:200 } console.log( "a:"+Iobj.a+" b:"+Iobj.b )//输出 a:100 b:200 类可以实现接口,实现接口的关键字是implements,其使用的示例代码如下: interface IRun {//定义接口 n:number run():void } class Run implements IRun {//实现接口 n:number run():void{ var i for( i=0; i< this.n; i++ ){ console.log( i ) } } constructor( n:number ) { //构造函数 this.n = n } } var obj = new Run( 6 )//创建对象 obj.run() 3.6模块 在处理模块化代码方面,JavaScript的不同方法有一定的历史,自TypeScript诞生以来,其实现了对多种格式的支持,但随着时间的推移,逐渐融合为ES6模块的格式上,其基本就是import/export语法。ES模块是在2015年被添加到JavaScript规范中的,到2020年得到了广泛的Web浏览器和JavaScript运行时支持。 TypeScript模块的设计理念是可以替换的代码块。模块是在其自身的作用域里执行,这样定义在模块里面的变量、函数和类等在模块外部是不可见的。如果需要在模块外可见,则需要明确地使用export导出。同时,使用时需要通过import导入所导出的模块中的变量、函数、类等。两个模块之间的关系是通过在文件级别上使用import和export建立的。 模块使用模块加载器去导入其他的模块。在运行时,模块加载器的作用是在执行此模块代码前去查找并执行这个模块的所有依赖。常用的加载器是服务于Node.js的CommonJS和Web应用的Require.js。 3.6.1模块导出与导入 对于ES模块语法,文件可以通过export default方式声明默认导出,示例代码如下: //ch03/hello.ts export default function helloWorld() { console.log("您好,世界!") } 在另外一个文件中,可以通过import导入模块,示例代码如下: //ch03/app1.ts import helloWorld from "./hello"//从文件hello.js导入模块 helloWorld();//调用函数 除了默认导出之外,可以通过export导出多个变量和函数等,示例代码如下: //ch03/maths.ts export var pi = 3.14 export let e = 2.718 export class RandomNumber { //这里省略了类实现代码 } export function abs(num: number) { if (num < 0){ return num * -1 } return num } 对于文件maths.ts的导出,可以在别的文件中导入,示例代码如下: //ch03/app2.ts import { pi, e, abs } from "./maths" console.log(pi) const a = abs( -100*e ) console.log(e) 导入时,使用import {old as new}可以为已有的标识符重命名,示例代码如下: //ch03/app3.ts import { pi as π } from "./maths";//以π作为pi的新名 console.log( π ); 导入时,使用* as name可以把所有导出的对象放入一个命名空间下,示例代码如下: //ch03/app4.ts import * as math from "./maths"//放到命名空间math下 console.log( math.pi )//通过命名空间访问pi const a = math.abs( math.e )//通过命名空间访问e 在ES6语法里,可以使用export和export default两种方式导出,二者的主要区别如下: (1) export为导出,export default为默认导出。 (2) 在一个文件或模块中,export、import可以有多个,但export default只能有一个,即默认导出只能有一个。 (3) 通过export方式导出,在导入时需要加花括号{ },如果使用export default导出,则在导入时不需要加花括号{ }。 例如,对于export导出的基本用法如下: //ch03/testa.ts export const s = "something" export function log( s ) { return s; } 对应的导入方式的示例代码如下: //ch03/testb.ts import { s, log } from './testa'//注意带花括号,也可以分开导入(两次) 例如,对于export default导出的基本用法如下: //ch03/testc.ts const s = "something" export default s//注意export default导出只能有一个 默认导出所对应的导入方式,示例代码如下: //文件testd.ts import s from './testc';//导入时不带花括号 export default s//export default导出只能有一个,这里再次导出s 当使用export default为模块指定默认输出时,可以不需要知道所要加载模块的标识符名称,示例代码如下: //ch03/teste.ts let s = "something" export default s //这里相当于为s变量值"something"起了一个系统默认的变量名default //default只能有一个值,所以一个文件内不能有多个export default 对于默认导出,在另一个文件中导入时,示例代码如下: //ch03/testf.ts import any1 from "./teste" import any2 from "./teste" //本质上,teste.ts文件的export default会输出一个叫作default的变量 //系统允许为它取任意名字且不需要用花括号包含 console.log( any1 )//输出something console.log( any2 )//输出something 3.6.2CommonJS模块用法 CommonJS是NPM上大多数模块的交付格式,其导出通过设置exports全局属性来导出的module,使用require语句导入文件,示例代码如下: //文件:testg.ts function abs(num: number) { if (num < 0) return num * -1; return num; } module.exports = { pi: 3.14, abs, } 对于以上文件代码,在其他文件中引入的示例代码如下: //ch03/testh.ts const maths = require("./testg"); console.log( maths.pi ) maths.abs( -3.6 ) 总之,模块可以很好地组织多文件应用。另外,关于模块的导入和导出还有其他的一些用法,这里不再阐述。 3.7装饰器 装饰器(Decorator)在TypeScript中是非常常用的,它可以使程序变得更加美好。许多库是基于装饰器构建的,如Angular、Nestjs等。这里简要地介绍装饰器以便读者能够更好地进行基于TypeScript的HarmonyOS应用开发。 装饰器在本质上是一种特殊的函数,可以被应用在类、类属性、类方法、类访问器、类方法的参数上,从而改变原有的功能。装饰器很像组合一系列函数,通过它可以轻松实现代理模式来使代码更简洁。 启用装饰器,可以通过命令行参数或在tsconfig.json文件中启用实验性装饰器编译器选项实现。通过命令行启用装饰器的命令如下: tsc --target ES6 --experimentalDecorators 对于配置文件tsconfig.json,启用装饰器的配置如下: { "compilerOptions": { "target": "ES6", "experimentalDecorators": true } } 装饰器在使用上语法十分简单,只需在想使用的装饰器前加上@符号,这样装饰器就会被应用到对应的目标上。下面是一个在类上使用装饰器的示例,代码如下: function test() {//一个函数 console.log("This is test") } @test//在类A上使用装饰器 class A {} 如果一个函数返回一个回调函数,当这个函数作为装饰器来使用时,这个函数就是装饰器工厂,示例代码如下: function test() { console.log('test out'); return (target) => { console.log('test in') } } @test() class A{ } 上方代码的含义为给A这个类绑定了一个装饰器工厂,在绑定时由于在函数后面写上了(),所以会先执行装饰器工厂以便获得真正的装饰器,真正的装饰器会在定义类之前执行,所以会接着执行里面的函数,上方代码输出的结果如下: test out test in 装饰器只在解释执行时会执行一次,下面是一个说明示例,代码如下: function f(C) { console.log('a decorator') return C } @f class A {} 以上代码输出a decorator,即使没有使用类定义对象,这里也会执行装饰器。 装饰器可以组合,即装饰器组合起来一起使用,组合使用的装饰器会从上至下地依次执行所有的装饰器工厂,获得所有真正的装饰器后,再从下至上地执行所有的装饰器,示例代码如下: function t1(target) { console.log('t1') } function t2(target) { console.log('t2') } function f1() { console.log('f1 out') return (target) => { console.log('f1 in') } } function f2() { console.log('f2 out') return function () { console.log('f2 in') } } @t1 @f1() @f2() @t2 class A { } 以上代码的执行结果如下: f1 out f2 out t2 f2 in f1 in t1 装饰器按照其应用的目标可以分为5种,分别为类装饰器、属性装饰器、方法装饰器、访问器装饰器、参数装饰器。5种不同的装饰器应用在不同的地方,使用5种装饰器的示例代码如下: @classDecorator//类装饰器 class Car { @propertyDecorator//属性装饰器 name: string; @methodDecorator//方法装饰器 accelerate( @parameterDecorator//参数装饰器 num: number ) {} @accessorDecorator//访问器装饰器 get speed() {} } 关于装饰器的更多阐述可以参阅官方网络链接https://www.typescriptlang.org/docs/handbook/decorators.html,通过装饰器可以改变现有定义的行为,装饰器的使用场景包括项前或项后回调、监听属性变化、监听方法调用、对方法参数进行转换、添加新方法、添加新属性、运行时类型检查、自动编解码、依赖注入等。通过装饰器可以简化编码,在HarmonyOS中基于ArkTS的开发中使用了很多定义的装饰器。 小结 本章简要地介绍了TypeScript的基础知识,包括基本类型、运算符、控制语句、函数、类、接口、模块、装饰器等。TypeScript是JavaScript的一个超集,TypeScript的最初目标是成为JavaScript程序的静态类型检查器,但随着其发展,TypeScript更适合开发大型应用。 HarmonyOS基于TypeScript进行了扩展,提出了ArkTS语言,并实现了声明式开发范式——方舟开发框架(ArkUI),是目前HarmonyOS应用开发的主推框架,因此开发者有必要掌握TypeScript语言基础。