1.初步认识TypeScript
Posted 徐自勉
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了1.初步认识TypeScript相关的知识,希望对你有一定的参考价值。
简介:typescript是C#之父主导的一门语言,本质上是向javascript语言添加了可选的静态类型和基于面向对象的诸多特性。相当于javascript的超集,其包含es6。由于是和C#之父创造的,所以这里我采用和C#对比的方式学习他们之间的不同点,和主流面向对象语言(C#,Java)中相符的性质将不作记录。
1. 数字类型:C#有一系列限定大小范围的int,short,int16,long等整形,还有float,double等小数类型,而ts(TypeScript简称,为方便,以下皆简称ts)只有一种数字类型代表C#上面提到的全部数字类型:number,
let a:number=0b1010; //二进制
let b:number=0o744; //八进制
let c:number=0xf00d;//十六进制
2. 字符串类型:ts是string,和C#一致,不过ts增加了一种独特的语法,有点类似于C#的StringBuild对象,可以在字符串中嵌入占位符。使用字符串反引号(`)来定义多行文本,和内嵌表达式${expr}
let words:string=`hello,${name}你好,欢迎你来到${address}`;
3. 元组类型:元组类型用来表示已知元素数量和类型的数组,各元素的类型不必相同。这个是ts独有的,感觉该类型不会在实际编程中有很大用处。
4. 任意值类型:是ts针对编程时类型不明确变量使用的一种数据类型,常用于一下三种情况:
4.1 变量的值会动态变化时,比如来之第三方代码库,任意值类型可以让这些变量跳过编译阶段的类型检查。
4.2 改写现有代码时,任意值允许在编译时可选择的包含或移除类型检查。
4.3 定义各种类型数据的数组时。
5. never类型:是其他类型(包括null和undefined)的子类型,代表从不会出现的值,理解这句话很重要,never类型的变量只能被never类型所赋值,在函数中它通常表现为抛出异常,或无法执行到终止点。
如:let y:never=(()=>{throw new Error(‘‘)})();
6. let申明:let是用于声明变量的,和javascript中的var区别是let只能在块级作用域内有效,一般常在函数的作用域定义。并且使用了let定义的变量就具有了C#中变量定义的诸多限制,如不能重复,不能声明之前使用。
6.1 ts使用let声明变量类型有一点需要注意:如果一个类型可能出现null或者undefined,可以用|来支持多种类型。
如:let x:number | undefined;
let y:string | null | undefined;
7. 结构:是Es6的一个重要特性,其作用就是将声明的一组变量与相同结构的数组或者对象的元素值一一对应,并将变量相对应元素进行赋值。
7.1 数组结构:
如:let input=[1,2];
let [first,second]=input;
可以得出:first==0,second==2;
也可作用与已声明的变量:[first,second]=[second,first]; //变量交换
7.2 对象结构:
如: let test={x:0,y:10,width:15,height:16};
let {x,y,width,height}=test;
8. 数参数定义:javascript里,被调函数的每个参数都是可选的,而在ts里面被调函数的每个参数都是必传,在编译时会检查每个函数是否传值。
8.1 可选参数:在参数名旁边加上?号可以是参数变成可选参数,可选参数必须位于必选参数的后面的位置:
如:function max(x:number,y?:number)
调用: max(2);,max(2,4); //正确方式
max(2,4,7); //错误方式
8.2 默认参数:默认参数就是在定义函数参数时直接给该参数赋予一个默认值,这样可以在调用函数时如果不传改默认参数,该参数就是默认值。默认参数不必放在必选参数位置之后。
如: function max(x:number,y=4)
调用:max(2),max(2,4),max(2,undefined) //正确 如果默认参数定义到了必选参数前面,用户必须显示的传入undefined。
max(2,4,7) //错误
8.3 剩余参数: 当同时需要操作多个参数,或者并不知道会有多少参数传递进来时,就可以使用剩余参数。剩余参数定义方式采用"..."号做外参数名前缀,参数类型(形参)必须使用数组类型,且只能是函数的最后一个参数,
这样调用函数时,可以传入多个实数,在函数体中可以通过形参数组取得所有传入的剩余参数。
9. 箭头函数:ts提供的箭头函数(=>),可以在函数创建时就绑定this,从而解决javascript中的this由于在代码中的不同调用方式而导致的this指代window对象或者undefined。
如:let gif={
gifts:["xzm","panmin","laop","choulp"],
giftPicker:function(){
return this.gifts[1]
}
}
调用:let pickGift=gif.giftPicker();
pickGift(); //报错,Cannot read property ‘5‘ of undefined(...)
使用箭头函数:
let gif={
gifts:["xzm","panmin","laop","choulp"],
giftPicker:function(){
return ()=> {this.gifts[1]
}
}
}
10. 类的构造函数:使用construction来作为构造函数名定义,派生类构造函数必须调用super(),他会调用基类的构造方法。
11. 参数属性:这里其实叫参数访问限定符更合适,有点类似C#的属性名前面的限定符(public,private,protected),只不过ts给他更多的意义,参数属性是通过给构造函数参数添加一个访问限定符,
它可以方便的在一个地方定义并初始化类成员,参数属性是一种语法糖,类似于:
class car{
public wheel:number;
construction(wheel:number){this.wheel=wheel; }
}
使用参数属性:
class car {
construction(public wheel:number){
this.wheel=wheel;
}
}
减少了一些原本啰嗦的代码量。
12. 模块概念:Es6引入了模块的概念,感觉他有点像C#的名称空间,Java的包,但是又有些区别,这些区别是javascript的语法原因。
12.1 首先,模块是自声明的,两个模块之间的关系是通过在文件级别上使用import和export来建立。任何包含顶级import或者export的文件都会被当成一个模块。
12.2 其次,模块在其自身的作用域里执行,而不是在全局作用域里,定义在一个模块里的变量,函数和类等,在模块外部是不可见的,除非明确的使用export到处它们,
类似的,如果想使用其他的模块导出的变量,函数,类和接口时,必须先通过import导入它们。
12.3 模块使用模块加载器去导入他的依赖,模块加载器在代码运行时会查找并加载模块间的所有依赖。在Angular中,常用的模块加载器有SystemJS和webpack.
13. 模块导出方式,分为以下三种:
13.1 导出声明:任何模块都能通过export关键字导出。
如:export const x=1;
exprot interface Indentity{}
exprot class Car{}
13.2 导出语句:当需要对导出的模块进行重命名时,可以使用导出语句。
如:
class car{}
export { car };
export { car as BigCar };
13.3 模块包装:当需要修改和扩展已有的模块,并导出供其他模块调用。
//导出原先的验证器,但做了重命名
export { ErpIdentityValidate as RegExpBaseZipCodeValidator } from "./ErpIndentityValidate";
//一个模块可以包裹多个模块,并把新的内容以一个新的模块导出
export * from "./IndentityValidate";
export * from "./ErpIdentityValidate";
14. 模块导入方式:模块导入与模块导出相对应,可以使用import关键字来导入当前模块依赖的外部模块。有如下几种方式:
14.1 导入一个模块: import { EnpIndentityValidate } from "./ErpIdentityValidate";
14.2 别名导入: import { EnpIndentityValidate as eiv } from "./ErpIdentityValidate";
14.3 对整个模块进行别名导入: import * as validate from "./ErpIdentityValidate";
15. 模块的默认导出:模块可以用default关键字实现默认导出的功能,每个模块可以有一个默认导出。
另外,类和函数声明可以直接省略导出名来实现默认导出,默认导出有力于减少调用方调用模块的层数,减少代码的冗余。
15.1 默认导出类示例:
export default class ErpIdentityValidate{};
导入:
import validate from "./ErpIdentityValidate";
15.2 默认导出函数示例:
export default function{};
导入:
import validate from "./ErpIdentityValidate";
使用导出的函数:
validate();
15.3 默认导出值:
export default "TypeScript";
导入:
import name from "./ErpIdentityValidate";
16. 模块设计原则:
16.1 尽可能的在顶层导出:顶层导出可以降低调用方使用的难度,过多的"."操作使得开发者要记住过多的细节,所以尽量使用默认导出(使用者可以直接导入对象)或者
顶层导出(顶层导出方便调用者一目了然模块有哪些可供导入的对象),尤其是单个对象可以采用默认导出方式。
16.2 明确的列出导入的名字:在导入的时候尽可能明确的指定导入对象的名称,这样只要接口不变,调用方式就可以不变,从而降低了导入跟导出的耦合度,做到面向接口编程。
如:import { cat,dog } from "./ErpIdentityValidate";
16.3 使用命名空间导入:
如://MyLargemodule.ts
export class Dog{}
export class Cat{}
export class Tree{}
导入: import * as myLargemodule from "./MyLargemodule";
let x=new myLargemodule.Dog();
16.4 使用模块包装进行扩展:当需要扩展一个模块的功能时,推荐的方案是不要去更改原来的对象,而是导入该对象,再继承该对象,扩展导出一个新的对象。
如:
export class M{}; //ModuleA.ts
import { ModuleA } from "./ModuleA";
export class ModuleB extends ModuleA{}
17. 接口:TypeScript接口的使用方式类似Java,同时还增加了更灵活的接口类型,包括属性,函数,可索引(indexable TypeScript)和类等类型。
17.1 属性类型接口:接口中只定义了属性的接口,实现该接口的方式只需要"形式上"的满足接口的要求即可,
如: interface FullName{
firstName:string;
secondName:string;
}
function printLabel(name:FullName){}
let obj={age:10,firstName:‘xzm‘,secondName:‘panmin‘}
printLabel(obj); //obj对象只需要包含一个firstName和secondName属性,且类型都是string既可。
17.1.2 可选属性,typescript提供了对可能存在的属性进行预定义,并兼容不传值的情况。其定义方式和普通接口没什么大的差别,就是可选属性变量名后添加一个?号。
如:interface FullName{
firstName:string;
secondName?:string;
}
function printLabel(name:FullName){}
let obj={firstName:"xzm"};
printLabel(obj);
17.2 函数类型接口:定义函数类型接口时,需要明确定义函数的参数列表和返回值类型,且参数列表的每个参数都要又参数名称和类型,不需要定义函数名。
如:interface Cat{
(name:string,salt:string):string
}
let tomcat:Cat;
tomcat=function(name:string,salt:string){return "miaomiao"}
注意:函数的参数名,和类型必须保持一致,同时函数的返回类型也必须保持一致。
17.3 可索引类型接口:用来描述那些可以通过索引得到的类型,它包含一个索引签名(类似数组的下标,字典的key),表示用来索引的类型与返回值类型,即通过特定的索引来得到指定类型的返回值。
这是一个在C#和Java中还没有的特性。索引签名支持字符串和数字两种数据类型,使用这两种类型的最终返回值是一样,即当使用数字类型来索引时,javascript最终也会将其转换成自字符串
类型后再去索引对象。
个人的感觉可索引类型接口,其实有点想指定格式类型的数组,或者字典(Map).
如:
inteface UserArray{
[index:number]:string; //索引类型数字类型,近似于数组
}
interface UserObject{
[index:string]:string; //索引类型是字符串类型,相当于字典
}
let userArray:UserArray;
let userObject:UserObject;
userArray=["zhangsan","lisi"];
userObject={"name1":"张三","name2":"李四"};
17.4 类类型接口:基本上和C#,Java里面的传统接口定义并无什么大的差别。类继承接口采用implements关键字。
17.5 接口扩展:接口也可以实现相互扩展,接口继承采用extends关键字。
18. 装饰器:(Decorators)是一种特殊类型的声明,它可以被附加到类声明,方法,属性或参数上。装饰器由@符号紧接一个函数名称,如@expression,expression求值后必须是一个函数,
在函数执行的时候装饰器的声明方法会被执行。装饰器是用来给附着的主体进行装饰,添加额外行为的。
个人见解:感觉typescript的装饰器就是Java的注解,C#的特性,算是一种元编程,定义在对象上,用于代码执行前后,做额外的事情,主要提供面向截面编程。
TypeScript官方介绍:装饰器(Decorators)为我们在类的声明及成员上通过元编程语法添加标注提供了一种方式。 Javascript里的装饰器目前处在
建议征集的第一阶段,但在TypeScript里已做为一项实验性特性予以支持。
注意:装饰器是一项实验性特性,在未来的版本中可能会发生改变。
若要启用实验性的装饰器特性,你必须在命令行或tsconfig.json里启用experimentalDecorators编译器选项:
命令行:
tsc --target ES5 --experimentalDecorators
tsconfig.json文件启用:
{
"compilerOptions": {
"target": "ES5",
"experimentalDecorators": true
}
}
18.1 装饰器求值:类中不同声明的装饰器将按以下规定的顺序应用:
1.参数装饰器,然后依次是方法装饰器,访问符装饰器,或属性装饰其应用到每个实例成员。
2.参数装饰器,然后依次是方法装饰器,访问符装饰器,或属性装饰其应用到每个静态成员。
3.参数装饰器应用到构造函数。
4.类装饰器应用到类。
18.2 类装饰器:在类声明之前声明,类装饰器应用于类的构造函数,可以用来监视,修改或替换类定义。
重点1:类装饰器表达式会在运行时当做函数被调用,类的构造函数被当做其唯一的参数。
重点2:如果类装饰器返回一个值,它会使用提供的构造函数来替换类的声明。(也就是说,可以用类装饰器返回一个函数的形式来动态替换类的原本构造函数)。
注意:
如:
@sealed
class Greeter{
greeter:string;
constructor(message:string){
this.greeter=message;
}
greeter(){
return "hello"+this.greeter;
}
}
@sealed装饰器定义如下
function sealed(constructor:Function){
Object.seal(constructor);
Object.seal(constructor.protected); //当@sealed被执行时,它将密封该了类的构造函数和原型。
}
18.3 方法装饰器:声明在一个方法的声明之前,它会被用到方法的属性描述符上(descriptor),可以用来监视,修改或替换方法定义。方法装饰器不能用在声明文件(.d.ts),
重载或者任何外部上下(比如declare的类中)中。
方法装饰器表达式会在运行时当作函数被调用,传入下列3个参数:
1.target:对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
2.properrtyKey:方法(成员)的名称。
3.descriptor:成员的属性描述符。
注意:如果代码的输出版本小于ES5,属性描述符将会是undefined。
如果方法装饰器的返回一个值,它会被用作方法的属性描述符,如果代码的输出目标小于ES5,返回值会被忽略。
其中descriptor类型为TypedPropertyDescriptor, 在typescript中定义如下:
interface TYpedPropertyDescriptor<T>{
enumerable?:boolean; //是否可遍历
configurable?:boolean; //属性描述是否可改变或者属性是否可删除
writable?:boolean; //是否可修改
value?:T; //属性的值
get?:()=>T; //属性的访问器函数(getter)
set?:(value:T)=>void //属性的设置器函数
}
如:
class TestClass{
@log
testMethod(arg:string){
return "xzm:"+arg;
}
}
装饰器@log的实现:
function log(target:Object,properrtyKey:string,descriptor:TypedPropertyDescriptor<any>){
let origin=descriptor.value; //通过方法属性描述符的value属性,取得有关方法对象
descriptor.value=function(...args:any[]){
console.log("args:"+JSON.stringify(args)); //调用前
let result=origin.apply(this,args); //调用方法
console.log("The result" + result);
return result;
}
return descriptor;
}
使用代码测试: new TestClass().testMethod("test method descorator");
结果输出如下: agrs:["test method descorator"]
The result-xzm:test method descorator
总结:个人感觉广发证劵团队在这里(揭秘Angular2这本书中)对方法装饰器讲得比较粗,对TypedPropertyDescriptor对象基本一笔带过,
但是示例中却用到了该对象内部的很多东西,使人跟本无从知道方法装饰器具有何作用。
18.4 访问器装饰器:访问器装饰器声明在一个访问器声明之前。访问器装饰器应用于访问器的属性描述符(),并且可以用来监视,修改或替换一个访问器定义。
注意:TypeScript不允许同时装饰一个成员的set或者get访问器。取而代之的是,一个成员的所有装饰必须应用在文档顺序的第一个访问器上。这是因为,
在装饰器应用于一个属性描述符时,它联合了get和set访问器,而不是分开声明的。
访问器装饰器表达式会在运行时当做函数被调用,传入下列3个参数:
1.target:对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
2.properrtyKey:方法(成员)的名称。
3.descriptor:成员的属性描述符。
访问器装饰器基本和方法装饰器一样,除了需要注意上面提到的不允许同时装饰一个成员的set和get访问器以外。
18.5 属性装饰器:属性装饰器声明在一个属性声明之前,属性装饰器表达式会在运行时当做函数被调用,传入下列两个参数:
1.target:对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
2.property:成员(属性)的名字。
注意:属性描述符不会作为参数传入属性装饰器,这于TypeScript是如何初始化属性装饰器有关。因为目前没有办法在顶一个原型对象的成员时描述一个实例的属性,
并且没办法监视或修改一个属性的初始化方法。因此属性描述符只能用来监视类中是否声明了某个名字的属性。
18.6 参数装饰器:声明在一个参数声明之前(用于的类的构造函数或方法声明),参数装饰器表达式会在运行是被当做函数调用。
1.target:对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
2.propertyKey:成员的名字。
3.parameterIndex:参数在函数参数列表中的索引。
注意:参数装饰器只能用来监视一个方法的参数是否被传入。参数装饰器在Angular中被广泛使用,特别是结合reflect-metadata库来支持实验性的Metadata API。
参数装饰器的返回值会被忽略。
18.7 装饰器组合:TypeScript支持多个装饰器同时应用到一个声明上,实现多个装饰器复合使用,语法支持从左到右,或从上到下书写。
在TypeScript里,当多个装饰器应用在一个声明上的时候,会进行如下步骤的操作:
1.从左到右(从上到下)依次执行装饰器函数,得到返回结果。
2.返回的结果会被当做函数,从左到右(从上倒下)依次调用。
19. 范型:基本和C#一致,ts的范型除了可以用于类上定义,还可以用于函数上定义。
20. TypeScript相关
20.1 编译配置文件:tsc编译器有很多命令行参数,都写在命令行上会十分繁琐。tsconfig.json文件正是用来解决这个问题。当运行tsc时,编译器从当前目录向上搜索tsconfig.json
文件来加载配置,类似于package.json文件的搜索方式..
20.2 TypeScript的一些语法糖
1.类型别名:类型别名声名 type sn= number | string; //sn就代表number和string类型.
2.使用interface往另一个interface里面添加额外成员.
如:interface Foo{ x: number;}
interface Foo{ y:number;}
let a:Foo...;
console.log(a.x+a.y); //ok
3.同上,interface还可以往一个类里面添加额外成员.但是不能用interface为类型别名里添加额外成员.
4.声明合并:是指编译器将针对同一个名字的多个独立声明合并为单一声明.和并后的声明同时拥有原先多个声明的特性.,其实这就包含了上面2,3两条.
注意:接口的非函数成员必须是唯一性,合并时不能出现重复的.对于函数成员,每个同名函数声明都会被当作函数重载,同时后面接口的函数比前面接口的函数具有更高的优先级.
5.名称空间合并:与接口合并相似,同名的命名空间也会合并其成员.命名空间可以与其他类型的声明进行合并,只要命名空间的定义符合将要合并类型的定义,合并后包含两者的声明类型.
示例1:
class Album { label:Album.AlblumLabel }
namespace Ablum{
export class AlbumLabel{};
}
必须导出AlblumLabel类,好让合并的类能访问.合并的结果是一个类并带有一个内部类.
示例2:
function buildLabel(name:string):string{
return buildLabel.prefix+name+buildLabel.suffix;
}
namespace buildLabel{
export let prefix="";
export let suffix="hello,";
}
6.非法合并:目前,类不能与其他类或变量合并.
7.全局声明:declare关键字可以将对象声明为全局.
以上是关于1.初步认识TypeScript的主要内容,如果未能解决你的问题,请参考以下文章