TypeScript学习笔记
《零》,概述
※,vscode 的智能提示用的是 TypeScript language service,这个服务有个叫 AutoAutomatic Type Acquisition(ATA)的东西,ATA会根据package.json中列出的npm modules拉取这些模块的类型声明文件(npm Type Declaration files,即 *.d.ts 文件),从而给出智能提示。
※,安装TypeScript 编译器: npm install -g typescript(全局安装) 或 npm install --save-dev typesript (仅将ts编译器安装在当前项目中)。如果想装特定版本的Ts可以使用npm install -g typescript@3.3.1
※,tsc -h 查看帮助文档。各种编译选项参数和其含义。
※,tsc --init 可以生成一个tsconfig.json配置文件(当然也可以手动创建此文件)。此文件配置了TypeScript项目信息,如编译选项, 包含的文件等等。没有此文件则使用默认设置。使用tsc命令编译ts文件时,ts编译器会自动查找 tsconfig.json,并按照其中的配置编译ts文件。
※,tsc xxx.ts用于编译某个ts文件,也可以直接运行tsc命令,编译所有的ts文件。如果有tsconfig.json文件,直接运行tsc将会编译tsconfig.json中包含的所有ts文件。
※,tsconfig.json中的一些配置项说明:
{
"compilerOptions": { "target": "es5", "module": "commonjs", "outDir": "out",//此属性配置了编译后js文件存储的位置
"sourceMap":true, //true表示编译后同时生成map文件,指示ts和js文件的对应关系。
},
"files":{
},
"extends":{
},
.....
}※,
《一》,文档学习 https://www.tslang.cn/docs/home.html
一,快速入门
1,要注意的是尽管 ts 中有错误(类型不匹配等),相应的js
文件还是被创建了。 就算你的代码里有错误,你仍然可以使用TypeScript。但在这种情况下,TypeScript会警告你代码可能不会按预期执行。
2,在TypeScript里,只要两个类型内部的结构兼容那么这两个类型就是兼容的。 这就允许我们在实现接口时候只要保证包含了接口要求的结构就可以(实现时可以比接口定义的字段多,但是不能比其少),而不必明确地使用 implements
语句。当然写上implements语句更清晰
3,类:它带有一个构造函数和一些公共字段,要注意的是,在构造函数的参数上使用public等同于创建了同名的成员变量。
二,基础类型:
1,数字:和JavaScript一样,TypeScript里的所有数字都是浮点数。 这些浮点数的类型是 number
。
2,字符串:可以使用模版字符串,它可以定义多行文本和内嵌表达式。 这种字符串是被反引号包围( `
),并且以${ expr }
这种形式嵌入表达式。如 `my name is ${myName}, I am ${age+11} years old`
2.1,JavaScript 中的String与string的区别:String是构造函数,而"string"是变量的一种类型.
JS的数据类型一般用大写的String,Number,Undefined,Null,Object来表示,而小写的类型的作用仅仅是在使用 typeof 和 instanceof 用来判断具体类型,或是作为返回的字符串,用来表明该类型是什么,是基本类型还是引用类型,其他地方就用不到了。 https://blog.csdn.net/fengwei4618/article/details/77955261
typeof String // "function" typeof string // "undefined" typeof "string" // "string"
3,数组:
TypeScript像JavaScript一样可以操作数组元素。 有两种方式可以定义数组。 第一种,可以在元素类型后面接上 []
,表示由此类型元素组成的一个数组:
let list: number[] = [1, 2, 3];
第二种方式是使用数组泛型,Array<元素类型>
:
let list: Array<number> = [1, 2, 3];
4,元组 Tuple
元组类型允许表示一个已知元素数量和类型的数组,各元素的类型不必相同。 比如,你可以定义一对值分别为 string
和number
类型的元组。
// Declare a tuple type
let x: [string, number];
// Initialize it
x = ['hello', 10]; // OK
// Initialize it incorrectly
x = [10, 'hello']; // Error
当访问一个已知索引的元素,会得到正确的类型:
console.log(x[0].substr(1)); // OK
console.log(x[1].substr(1)); // Error, 'number' does not have 'substr'
当访问一个越界的元素,会使用联合类型替代:
x[3] = 'world'; // OK, 字符串可以赋值给(string | number)类型
console.log(x[5].toString()); // OK, 'string' 和 'number' 都有 toString
x[6] = true; // Error, 布尔不是(string | number)类型
联合类型是高级主题,我们会在以后的章节里讨论它。
5,枚举
6,Any
7,Void
8,Null 和Undefined: Null是没有在内存中开辟空间,Undefined是开辟了空间但是里面没有存值。
9,Never
10,Object:object
表示非原始类型,也就是除number
,string
,boolean
,symbol
,null
或undefined
之外的类型。
declare function create(o: object | null): void; create({ prop: 0 }); // OK create(null); // OK create(42); // Error create("string"); // Error create(false); // Error create(undefined); // Error
11,类型断言
类型断言好比其它语言里的类型转换。
类型断言有两种形式。 其一是“尖括号”语法:
let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;
另一个为as
语法:
let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;
两种形式是等价的
三,变量声明
先猜一下下面的代码会输出什么结果?
for (var i = 0; i < 10; i++) { setTimeout(function() { console.log(i); }, 100 * i); } 结果是打印出10个10!解释如下:setTimeout在若干毫秒后执行一个函数并且是在for循环结束后。for循环结束后,i的值为10.所以当函数被调用的时候,它会打印出10! 如果想打印出1~9,有两个方法,1,将var变成let关键词(ES6才开始支持)或者 2,使用立即执行函数捕捉每次循环的i的值。代码如下: for (var i = 0; i < 10; i++) { (function(j){ setTimeout(function () { console.log(j); }, j * 10); })(i); }
1,关于变量作用域的一些说明:在ES6之前只有全局作用域和函数作用域,ES6新增了块级作用域。 每次JavaScript引擎进入到一个作用域后(对于var声明的变量,这个作用域是函数作用域;对于let声明的变量,这个作用域是块级作用域----if块,for循环块等等),它都会创建一个 变量 的环境。就算作用域内代码已经执行完毕,这个环境与引擎 捕获的变量依然存在。JavaScript引擎的资源回收机制 会在合适的时机自己回收 这个环境和 变量。当let声明出现在循环体中时(即有了块级作用域的概念),不仅是在循环里引入了一个新的变量环境,而且针对每次迭代都会创建这样一个新作用域。这就是我们使用立即执行函数表达式做的事情。
2,const声明 是声明变量的另一种方式。const声明与let声明有相同的作用域规则(块级作用域),和let的区别是 const声明的变量在赋值后不能再被改变。注意一点:如果用const声明一个引用类型,比如对象 const o = {name:"xx", age:12}, 声明后可以改变变量o的内部状态,如设置 o.name = "yyy"是被允许的。
3, 使用var可以重复声明一个变量,最终你只会得到一个。 但是使用let声明变量 在一个作用域内只能声明一个同名变量。见下面的例子:
例一: let a = 3; if (true) { let a = 4;//块级作用域中声明的变量只在 这个块级 起作用。 } console.log(a);//结果输出是3 例二: function f(condition, x) { if (condition) { let x = 100;//块级作用域中声明的变量只在此块中有意义,此块中的x覆盖了外面作用域的x. return x; } return x; } console.log(f(true, 0));//输出100 console.log(f(false, 0));//输出0
另外,如果块级作用域里用到了一个变量,但是在块里没有此变量的声明,则引擎会继续向上找此块所在的函数(或更高一级的块等),如果还没有引擎继续向上找,如果一直找不到就会报错
4,解构赋值
- 数组解构:数组中变量和右边数组尽可能一一对应。
let [first, second] = [3,4];//first=3,second=4; [second, first] = [first, second];//first=4,second=3; let [first] = [1,2,3,4];//first = 1; let [,first, second] = [1,2,3,4];//first=2,second=3; let [,first,,second] = [1,2,3,4];//first=2,second=4; let [first,,second] = [[21,22,23],3]//first=[21,22,23],second=undefined let [first];//报错 let first;//first=undefined let [first,second]=[3];//first=3,second=undefined //用 ... 语法创建剩余变量。 let [first, ...rest] = [1,2,3,4,6];//first=1,rest=[2,3,4,6];
[first, second] = [11, 22];//注意first,second没有声明过,first=11,second=22;
- 对象解构赋值
let o = { c: "foo", b: 12, a: "bar" }; let {a, b, d} = o;//a=bar, b=12, d=undefined; //用 ... 语法创建剩余变量 let { a, ...rest } = o;//a=bar,rest={ c: 'foo', b: 12 }; /* * 就像数组解构,可以使用未声明的赋值,注意要将整个赋值用()括起来, * 因为JavaScript通常会将以 { 开头的语句解析为一个块。 */ ({ e, f } = { e: "contemplate", f: "思考, 沉思" });//e=contemlate,f="思考, 沉思"
5,展开Spread: 展开和解构是相反的操作。
let first = [1, 2]; let second = [3, 4]; let obj = { a: "aaa", b: "bbb" }; let spread = [...first, ...second, { ...obj, a: "ambiance" }]; let spread2 = [...first, ...second, { a: "ambiance", ...obj }] //注意,展开对象时,同名的key,后面的将覆盖前面的。 console.log(spread);//[ 1, 2, 3, 4, { a: 'ambiance', b: 'bbb' } ] console.log(spread2);//[ 1, 2, 3, 4, { a: 'aaa', b: 'bbb' } ] //展开是浅拷贝(shallow copy),不会改变原变量 console.log(first);//[1,2]; //展开对象时,它只能展开对象 自身的可枚举属性,不可枚举属性以及方法就不能展开了 class C { p = 12; m() { } } let c = new C(); let clone = { ...c }; clone.p; // ok clone.m(); // error!
四,接口
0,如果一个变量定义为某个接口定义的类型,那么
- 此变量的值必须含有接口中定义的所有属性和方法,不能少!当然不包含可选属性。
- 如果赋给变量的值是字面量,由于字面量在TypeScript中会受到额外的类型检查,此字面量里含有的属性或方法不能少,同时也不能多,即不能含有接口中不包含的属性或方法。
- 如果赋给变量的值是另一个变量,则另一个变量中可以含有接口中不存在的方法或属性。即不可以少,但是能多。
- 如果接口里面什么都没有,会有特殊规则。没什么实际价值。
1,可选属性
interface SquareConfig { color?: string; width?: number; }
2,只读属性
※,TypeScript 具有ReadOnlyArray<T>,它与Array<T>
相似,只是把所有可变方法去掉了,因此可以确保数组创建后再也不能被修改:
let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a;
ro[0] = 12; // error!
ro.push(5); // error!
ro.length = 100; // error!
a = ro; // error!
上面代码的最后一行,可以看到就算把整个ReadonlyArray
赋值到一个普通数组也是不可以的。 但是你可以用类型断言重写:
a = ro as number[];
※,readonly
vs const
最简单判断该用readonly
还是const
的方法是看要把它做为变量使用还是做为一个属性。 做为变量使用的话用 const
,若做为属性则使用readonly
。
3,额外的属性检查
TypeScript中的对象字面量会被特殊对待:当将它们赋值给变量或作为参数传递的时候,它们会经过 额外属性检查。如果一个对象字面量存在任何“目标类型”不包含的属性时,你会得到一个错误。
interface SquareConfig { color?: string; width?: number; } function createSquare(config: SquareConfig): { color: string; area: number } { // ... } //注意,colour不是color,这里会得到一个错误:SquareConfig接口里不存在colour属性。 let mySquare = createSquare({ colour: "red", width: 100 });
绕开这些额外的属性检查有以下几种方法:
※,使用类型断言
let mySquare = createSquare({ width: 100, opacity: 0.5 } as SquareConfig);
※,添加一个字符串索引签名(后面会讲)
interface SquareConfig { color?: string; width?: number; [propName: string]: any; } 这里索引签名表示的是SquareConfig可以有任意数量的属性,并且只要它们不是color和width,那么就无所谓它们的类型是什么。
※,将对象字面量赋值给一个变量,因为变量不会像对象字面量一样会经过额外的属性检查。
let squareOptions = { colour: "red", width: 100 }; let mySquare = createSquare(squareOptions);
※,
4,函数类型
※,接口能够描述JavaScript中对象拥有的各种各样的外形。 除了描述带有属性的普通对象外,接口也可以描述函数类型。为了使用接口表示函数类型,我们需要给接口定义一个调用签名。 它就像是一个只有参数列表和返回值类型的函数定义。参数列表里的每个参数都需要名字和类型。
//函数类型接口定义了一个函数的各参数类型以及返回值类型 interface SearchFunc { (source: string, substring: string): boolean; } let mySearch: SearchFunc; //函数参数名称可以和接口中不一致 mySearch = function (src: string, sub: string): boolean { return src.search(sub) > -1; } //也可以不指定函数参数和返回值的类型,TypeScript的类型系统会自己推断出参数和返回值类型 mySearch = function (src, sub) { return src.search(sub) > -1; }
※,
5,可索引的类型(索引签名)
※,和描述函数类型差不多,TypeScript还可以用接口描述那些 “通过索引得到” 的类型,比如a[10], ageMap['Evan']等。接口中定义了一个索引签名,描述了索引的类型(如10,"Evan"的类型)以及索引的返回值类型(如a[10],ageMap["Evan"]的类型)。
//定义一个StringArray接口,它具有一个索引签名(用中括号括起来的这个形式的就是索引签名)。这个索引签名表示当使用一个number类型索引StringArray时会得到string类型的返回值。 interface StringArray { [index: number]: string; } let myArray: StringArray; myArray = ["Bob", "Fred"]; let myStr: string = myArray[0];
※,TypeScript支持两种索引签名:字符串索引和数字索引。 可以同时使用两种类型的索引,但是数字索引的返回值必须是字符串索引返回值类型的子类型。 这是因为当使用 number
来索引时,JavaScript会将它转换成string
然后再去索引对象。 也就是说用 100
(一个number
)去索引等同于使用"100"
(一个string
)去索引,因此两者需要保持一致。
class Animal { name: string; } class Dog extends Animal { breed: string; } // 错误:使用数值型的字符串索引,有时会得到完全不同的Animal! interface NotOkay { [x: number]: Animal; [x: string]: Dog; } let h:NotOkay = {}; h[100] = new Animal(); h["100"] = new Dog(); console.log(h[100]);//本想得到Animal{},输出的却是Dog{}
※,一个例子:
interface NumberDictionary { //这个是类型的索引签名,索引是string类型,索引返回值是number类型。 [index: string]: number; length: number; // 可以,length是索引string类型,其索引后的返回值是number类型 name: string // 错误,`name`的类型与索引类型返回值的类型不匹配 }
※, 将索引签名设置为只读,这样就防止了给索引赋值:
interface ReadonlyStringArray {
readonly [index: number]: string;
}
let myArray: ReadonlyStringArray = ["Alice", "Bob"];
myArray[2] = "Mallory"; // error!
你不能设置myArray[2]
,因为索引签名是只读的
6,类类型
※,类可以用关键字implements实现一个接口,实现接口时,接口里的属性或方法类中必须有,也可以定义接口中不存在的属性或方法。即可以多不可以少。
※,类静态部分与实例部分的区别(有些难理解)
当你操作类和接口的时候,你要知道类是具有两个类型的:静态部分的类型和实例的类型。 你会注意到,当你用构造器签名去定义一个接口并试图定义一个类去实现这个接口时会得到一个错误:
interface ClockConstructor {
new (hour: number, minute: number);
}
class Clock implements ClockConstructor {
currentTime: Date;
constructor(h: number, m: number) { }
}
这里因为当一个类实现了一个接口时,只对其实例部分进行类型检查。 constructor存在于类的静态部分,所以不在检查的范围内。
因此,我们应该直接操作类的静态部分。 看下面的例子,我们定义了两个接口, ClockConstructor
为构造函数所用和ClockInterface
为实例方法所用。 为了方便我们定义一个构造函数 createClock
,它用传入的类型创建实例。
interface ClockConstructor {
new (hour: number, minute: number): ClockInterface;
}
interface ClockInterface {
tick();
}
function createClock(ctor: ClockConstructor, hour: number, minute: number): ClockInterface {
return new ctor(hour, minute);
}
class DigitalClock implements ClockInterface {
constructor(h: number, m: number) { }
tick() {
console.log("beep beep");
}
}
class AnalogClock implements ClockInterface {
constructor(h: number, m: number) { }
tick() {
console.log("tick tock");
}
}
let digital = createClock(DigitalClock, 12, 17);
let analog = createClock(AnalogClock, 7, 32);
因为createClock
的第一个参数是ClockConstructor
类型,在createClock(AnalogClock, 7, 32)
里,会检查AnalogClock
是否符合构造函数签名。
7,继承接口
※,一个接口可以继承多个接口。
8,混合类型 (有点绕)
※,
9,接口继承类
※,当接口继承了一个类类型时,它会继承类的成员(即属性和方法)但不包括其实现。 就好像接口声明了所有类中存在的成员,但并没有提供具体实现一样。 接口同样会继承到类的private和protected成员。 这意味着当你创建了一个接口继承了一个拥有私有或受保护的成员的类时,这个接口类型只能被这个类或其子类所实现(implement)。
五,类
1,继承
※,和Java一样,TypeScript不允许多继承。一个类只能继承一个类。
※, 父类也叫基类,超类。子类也叫派生类。
※,子类继承了父类,如果子类中没有构造函数则会自动执行父类的构造函数。如果子类 中有构造函数,则它 必须 调用 super(parameters)
,它会执行基类的构造函数。 而且,在构造函数里访问 this
的属性之前,我们 一定 要调用 super(parameters)
。 这个是TypeScript强制执行的一条重要规则。
※,访问修饰符(public ,protected, private):public 修饰符可以省略。protected修饰的属性和方法可以在声明其的类内部 以及 继承了这个类的子类内部用this调用。实例化后不能调用此属性或方法。 private修饰的属性或方法只能在声明其的类内部用this调用。如果一个类的构造函数被标记为protected,那么这个类不能被实例化,但是可以被继承。如果一个类的构造函数被标记为private,那么这个类不能被实例化,也不能被继承。
TypeScript使用的是结构性类型系统。 当我们比较两种不同的类型时,并不在乎它们从何处而来,如果所有成员的类型都是兼容的,我们就认为它们的类型是兼容的。
然而,当我们比较带有 private
或 protected
成员的类型的时候,情况就不同了。 如果其中一个类型里包含一个private
成员,那么只有当另外一个类型中也存在这样一个 private
成员, 并且它们都是来自同一处声明时,我们才认为这两个类型是兼容的。 对于 protected
成员也使用这个规则。
下面来看一个例子,更好地说明了这一点:
class Animal {
private name: string;
constructor(theName: string) { this.name = theName; }
}
class Rhino extends Animal {
constructor() { super("Rhino"); }
}
class Employee {
private name: string;
constructor(theName: string) { this.name = theName; }
}
let animal = new Animal("Goat");
let rhino = new Rhino();
let employee = new Employee("Bob");
animal = rhino;
animal = employee; // 错误: Animal 与 Employee 不兼容.
※,可以使用readonly修饰符修饰类的属性,将其标记为只读属性。readonly不能修饰类的方法。
※,
2,参数属性
※,如果一个类的构造函数的参数有public, protected,private修饰符,那么就相当于类定义了一个同名属性,同时构造函数里给其赋了值。只有readonly修饰参数也可以,相当于public readonly xxx.
class Person { constructor(public name: string) { } } //以上类等同于如下 class Person { public name: string; constructor(name: string) { this.name = name; } }
※,
3,存取器(get和set方法)
※,类中的属性可以通过getters 和 setters 来控制对此属性的访问。比如,设置一个属性的值时,如果需要满足一定的条件才能设置,那么此时可以通过getters/setters。只带有get方法而不带有set方法的属性被自动推断为readonly属性。存取器的例子如下:
let passcode = "secret passcode"; class Employee { private_fullName: string; get fullName(): string { return this._fullName; } set fullName(newName: string) { if (passcode && passcode == "secret passcode") { this._fullName = newName; } else { console.log("Error: Unauthorized update of employee!"); } } } let employee = new Employee(); employee.fullName = "Bob Smith"; if (employee.fullName) { alert(employee.fullName); }
※,
4,静态属性
※,TypeScript的静态属性或方法只能通过类名访问。没有self之类的东西
※,
5,抽象类
※,抽象类作为其它派生类的基类使用。抽象类不能被实例化。不同于接口,抽象类可以包含成员的实现细节。
※,抽象类中可以定义抽象方法(抽象方法只能出现在抽象类中,普通类中不能定义抽象方法),抽象方法和接口语法差不多,只有方法签名,没有实现。但是和接口不一样的是,抽象方法必须有关键字abstract,实际相当于public abstract,也可以是protected abstract,但是不能是private abstract,因为抽象方法必须被派生类实现(但是抽象类中的非抽象方法不需要一定被派生类实现)。
※,
6,
7,
8,
9,
六,函数
在TypeScript里,虽然已经支持类,命名空间和模块,但函数仍然是主要的定义 行为的地方。 TypeScript为JavaScript函数添加了额外的功能,让我们可以更容易地使用。
1,函数的类型定义:
※,返回值类型:TypeScript能够根据返回语句自动推断出返回值类型,因此我们通常省略它。
※,函数类型:如果定义一个变量为函数类型,其完整的写法是:
let myAdd: (baseValue: number, increment: number) => number = function(x: number, y: number): number { return x + y; }; 还有一种写法,用带有调用签名的对象字面量来说明函数类型, //除了使用 (x:string)=>string,还可以如下: let sayHello: { (x: string): string } = function (theName: string): string { console.log("hello " + theName); return "hello " + theName; }; console.log(sayHello); sayHello("Evan")
※,类型推断:如果你在赋值语句的一边指定了类型但是另一边没有类型的话,TypeScript编译器会自动识别出类型:
// TypeScript根据右边值得类型推断变量myAdd的完整类型。 let myAdd = function(x: number, y: number): number { return x + y; }; // TypeScript根据myAdd的类型推断变量x,y的类型为number,其返回值也是number let myAdd: (baseValue: number, increment: number) => number = function(x, y) { return x + y; }; //甚至还可以这么写(不推荐)。TypeScript会根据myAdd的类型推断出函数有两个number类型的参数,调用myAdd时如果参数个数或类型不对会报错,但是这个样子声明这个函数时是不会报错的。 let myAdd: (baseValue: number, increment: number) => number = function () {return 33;}
※,
2,可选参数和默认参数 (ps,Java中不支持默认参数,Java解决此问题的方法是方法重载!)
※,TypeScript的函数,调用时传入的参数个数和声明时的参数个数要保持一致(JavaScript中没有硬性要求)。
※,可选参数:在函数声明时,参数后面加个 “?”可表明此参数是可选的。注意,可选参数 必须放在 必选参数 后面。
※,默认参数:在函数声明时,参数可以初始化一个默认值,当调用时没有传入参数或传了一个undefined(注意,传null时此参数值为null而不使用默认值)时,此参数默认使用默认值。默认参数不必在必须参数后面。如果带默认值的参数出现在必须参数之前,则调用时必须明确的传入undefined值来获取默认值。
※,可选参数 和 放在必须参数后面的默认参数 共享参数类型:(注意是有条件的,如果默认参数是在必须参数的前面,此时默认参数是必须要传的,则和可选参数不再共享参数类型)
function buildName(firstName: string, lastName?: string) {
// ...
}
和
function buildName(firstName: string, lastName = "Smith") {
// ...
}
共享同样的类型(firstName: string, lastName?: string) => string
。 默认参数的默认值消失了,只保留了它是一个可选参数的信息。
※,
3,剩余参数
※,JavaScript里,可以使用 arguments 来访问所有传入的参数。在TypeScript里,可以使用剩余参数将所有参数收集到一个变量里。剩余参数会被当做个数不限的可选参数。 可以一个都没有,同样也可以有任意个。 编译器创建参数数组,名字是你在省略号( ...
)后面给定的名字,你可以在函数体内使用这个数组。
这个省略号也会在带有剩余参数的函数类型定义上使用到:
function buildName(firstName: string, ...restOfName: string[]) {
return firstName + " " + restOfName.join(" ");
}
let buildNameFun: (fname: string, ...rest: string[]) => string = buildName;
※,
4,this (函数中的this)
※,JavaScript里,this的值是在函数被调用的时候才确定的。这是个既强大又灵活的特点,但是需要程序员弄清楚函数调用的上下文,而这并不是一件简单的事情。看一个例子:
let deck = { suits: ["hearts", "spades", "clubs", "diamonds"], cards: Array(52), createCardPicker: function() { return function() { let pickedCard = Math.floor(Math.random() * 52); let pickedSuit = Math.floor(pickedCard / 13); return {suit: this.suits[pickedSuit], card: pickedCard % 13}; } } } let cardPicker = deck.createCardPicker(); let pickedCard = cardPicker(); console.log("card: " + pickedCard.card + " of " + pickedCard.suit); ※,运行后发现会报错!调用cardPicker()的时候报错了,因为这里只是独立的调用了cardPicker(),顶级的非方法式调用会将this视为window对象(注意:严格模式下,this为undefined而不是window对象), ※,JavaScript(ECMAScript 6或叫ECMAScript 2015之前)解决这个问题的方法有两个:bind() 和 call()/apply(),使用如下: 1,call() let cardPicker = deck.createCardPicker(); let pickedCard = cardPicker.call(deck);//指定函数cardPicker在deck的作用域上调用。 2,bind() let cardPicker = deck.createCardPicker(); let newCardPicker= cardPicker.bind(deck);//bind()返回一个改变了作用域的函数。 let pickedCard = newCardPicker(); 以上两种方式都可以得到预想中的结果。
※,this
和 箭头函数(ECMAScript 6中的语法):
基础:各种特殊形式的箭头函数,比如a=>a, ()=>a, a=>({foo:"bar"})//单语句返回一个对象时,不能直接用{},要用()括起来。
进阶[非常非常非常好的解析了箭头函数中的this的一般性规则]
【自己总结】箭头函数并不绑定 this,arguments,super(ES6),抑或 new.target(ES6)。在箭头函数上使用call()
或者apply()
调用箭头函数时,无法对this
进行绑定,即传入的第一个参数被忽略。箭头函数中的this的值 确切来说就是整个箭头函数所在位置 处的那个地方的this值。
//例一 function foo() { setTimeout(() => { console.log(this); console.log("id:", this.id); }, 100); } /** * 解析:整个箭头函数在setTimeout()的参数里,这里的this就是foo函数作用域的this,也就是谁调foo函数,this就是谁 */ foo();//this是window对象,this.id:undefined foo.call({id:33});//this是传入的对象{id:33},this.id:33.注意这里是对foo应用call()而不是对箭头函数应用call() //例二: let test = () => { console.log(this); console.log(this.age); } /** * 解析:整个箭头函数就在全局的作用域里,箭头函数中的this就是全局作用域的this,即window对象 */ test.call({ age: 333 });//对箭头函数应用call()无法绑定this,即传入的对象会被忽略。this是window对象,this.age:undefined
在ECMAScript 以及 TypeScript中,可以使用箭头函数 使函数被返回时就已经绑定好正确的this值。箭头函数能保存函数创建时的this值而不是调用时的值。【箭头函数的this始终指向函数定义时的this,而非执行时】。
let deck = { suits: ["hearts", "spades", "clubs", "diamonds"], cards: Array(52), createCardPicker: function() { // NOTE: the line below is now an arrow function, allowing us to capture 'this' right here。下面的箭头函数可以在这里就捕获到this的值,而不是调用时才确定this的值。 return () => { let pickedCard = Math.floor(Math.random() * 52); let pickedSuit = Math.floor(pickedCard / 13); return {suit: this.suits[pickedSuit], card: pickedCard % 13}; } } } let cardPicker = deck.createCardPicker(); let pickedCard = cardPicker(); alert("card: " + pickedCard.card + " of " + pickedCard.suit);
※,给函数加上虚假的this参数:
可以给函数加上一个this参数,这个参数是虚假的,必须放在所有参数的最前面。这个参数不会影响其他的参数,也不算做参数的个数。这么做的作用是可以指出this的类型,明确指出这个函数只能在this对应的对象上调用。
interface Person { //第一个this不算参数,这个方法需要一个参数 getName(this:Person, theName:string):string; } let p1: Person = { //这里第一个参数也可以写为 this:Person,并且推荐这么写,因为可以明确指出这个函数只能在Person对象上调用。 getName: function (theName:string) { console.log(this); return theName; } } //调用时只需要一个参数即可。 p1.getName("Evan Tong");
※,TypeScript中的函数重载。
JavaScript中函数的签名(名字,参数个数,参数类型等)很灵活也很随意,但是在TypeScript这种类型系统的语言中,这种做法就不太好了,比如调用时无法知晓具体的参数个数即类型。TypeScript支持函数重载。方法如下:
let suits = ["hearts", "spades", "clubs", "diamonds"]; /** * 重载列表位置必须放在具体的实现的函数之前。之间不允许写任何其他代码。 * 这里重载列表是是前2个,第三个不是重载列表的一部分。 * 重载后pickCard函数在调用的时候会进行正确的类型检查,参数是一个对象或一个数字,以其他参数调用pickCard都会产生错误 * 为了让编译器能够选择正确的检查类型,它与JavaScript里的处理流程相似。 它查找重载列表,尝试使用第一个重载定义。 * 如果匹配的话就使用这个。 因此,在定义重载的时候,一定要把最精确的定义放在最前面。 */ function pickCard(x: { suit: string; card: number; }[]): number; function pickCard(x: number): { suit: string; card: number; }; function pickCard(x): any { // Check to see if we're working with an object/array // if so, they gave us the deck and we'll pick the card if (typeof x == "object") { let pickedCard = Math.floor(Math.random() * x.length); return pickedCard; } // Otherwise just let them pick the card else if (typeof x == "number") { let pickedSuit = Math.floor(x / 13); return { suit: suits[pickedSuit], card: x % 13 }; } } let myDeck = [{ suit: "diamonds", card: 2 }, { suit: "spades", card: 10 }, { suit: "hearts", card: 4 }]; let pickedCard1 = myDeck[pickCard(myDeck)]; console.log("card: " + pickedCard1.card + " of " + pickedCard1.suit); let pickedCard2 = pickCard(15); console.log("card: " + pickedCard2.card + " of " + pickedCard2.suit);
※,
七,泛型 (Generics)
1,泛型之Hello World
/* * T是类型变量,它是一种特殊的变量,只用于表示类型而不是值。可以理解为函数接受两种参数:类型参数T(用中括号括起来) 和 参数arg。这个函数叫做泛型函数 */ function identity<T>(arg: T): T { return arg; }
function loggingIdentity<T>(arg: T[]): T[] {
console.log(arg.length); // Array has a .length, so no more error
return arg;
}
你可以这样理解loggingIdentity
的类型:泛型函数loggingIdentity
,接收类型参数T
和参数arg
,它是个元素类型是T
的数组,并返回元素类型是T
的数组。 如果我们传入数字数组,将返回一个数字数组,因为此时 T
的的类型为number
。 这可以让我们把泛型变量T当做类型的一部分使用,而不是整个类型,增加了灵活性。
我们也可以这样实现上面的例子:
function loggingIdentity<T>(arg: Array<T>): Array<T> {
console.log(arg.length); // Array has a .length, so no more error
return arg;
}
2,泛型函数的类型
泛型函数的类型与非泛型函数的类型没什么不同,只是有一个类型参数在最前面,像函数声明一样:
function identity<T>(arg: T): T {
return arg;
}
let myIdentity: <T>(arg: T) => T = identity;
我们也可以使用不同的泛型参数名,只要在数量上和使用方式上能对应上就可以。
function identity<T>(arg: T): T {
return arg;
}
let myIdentity: <U>(arg: U) => U = identity;
我们还可以使用带有调用签名的对象字面量来定义泛型函数:
function identity<T>(arg: T): T {
return arg;
}
let myIdentity: {<T>(arg: T): T} = identity;
这引导我们去写第一个泛型接口了。 我们把上面例子里的对象字面量拿出来做为一个接口:
interface GenericIdentityFn {
<T>(arg: T): T;
}
function identity<T>(arg: T): T {
return arg;
}
let myIdentity: GenericIdentityFn = identity;
一个相似的例子,我们可能想把泛型参数当作整个接口的一个参数。 这样我们就能清楚的知道使用的具体是哪个泛型类型(比如: Dictionary<string>而不只是Dictionary
)。 这样接口里的其它成员也能知道这个参数的类型了。
interface GenericIdentityFn<T> {
(arg: T): T;
}
function identity<T>(arg: T): T {
return arg;
}
let myIdentity: GenericIdentityFn<number> = identity;
注意,我们的示例做了少许改动。 不再描述泛型函数,而是把非泛型函数签名作为泛型类型一部分。 当我们使用GenericIdentityFn
的时候,还得传入一个类型参数来指定泛型类型(这里是:number
),锁定了之后代码里使用的类型。 对于描述哪部分类型属于泛型部分来说,理解何时把参数放在调用签名里和何时放在接口上是很有帮助的。
除了泛型接口,我们还可以创建泛型类。 注意,无法创建泛型枚举和泛型命名空间。
※,
3,泛型类
4,泛型约束
※,
interface Lengthwise { length: number; } function loggingIdentity<T extends Lengthwise>(arg: T): T { console.log(arg.length); // Now we know it has a .length property, so no more error return arg; }
※,你可以声明一个类型参数,且它被另一个类型参数所约束。 比如,现在我们想要用属性名从对象里获取这个属性。 并且我们想要确保这个属性存在于对象 obj
上,因此我们需要在这两个类型之间使用约束。
function getProperty<T, K extends keyof T>(obj: T, key: K) { return obj[key]; } let x = { a: 1, b: 2, c: 3, d: 4 }; getProperty(x, "a"); // okay getProperty(x, "m"); // error: Argument of type 'm' isn't assignable to 'a' | 'b' | 'c' | 'd'.
※,
5,在泛型里使用类类型
6,
八,枚举(Enums)
1,TypeScript支持两种枚举:数字的和基于字符串的枚举。
※,字符串枚举必须给每个枚举成员一个初始化的值。
※,每个枚举成员都有一个值,值可以分为两种类型:计算出来的 和 常量的。
※,数字枚举 有反向映射机制(即根据枚举属性名可以得到枚举成员的值,也可以反过来,根据枚举成员的值得到枚举成员的名称。),而字符串枚举没有反向映射。
※,
2,常量枚举(const enums)
※,常量枚举通过在枚举上使用 const
修饰符来定义。
※,常量枚举成员的值只能是常量的,不能是计算出来的。不同于常规的枚举,它们在编译阶段会被删除。 常量枚举成员在使用的地方会被内联进来。 之所以可以这么做是因为,常量枚举不允许包含计算成员。
const enum Directions {
Up,
Down,
Left,
Right
}
let directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right]
生成后的代码为:
var directions = [0 /* Up */, 1 /* Down */, 2 /* Left */, 3 /* Right */];
※,
3,
九,类型兼容性
1,介绍
TypeScript里的类型兼容性是基于结构子类型的。 结构类型是一种只使用其成员来描述类型的方式。 它正好与名义(nominal)类型形成对比。(译者注:在基于名义类型的类型系统中,数据类型的兼容性或等价性是通过明确的声明和/或类型的名称来决定的。这与结构性类型系统不同,它是基于类型的组成结构,且不要求明确地声明。) 看下面的例子:
interface Named {
name: string;
}
class Person {
name: string;
}
let p: Named;
// OK, because of structural typing
p = new Person();
在使用基于名义类型的语言,比如C#或Java中,这段代码会报错,因为Person类没有明确说明其实现了Named接口。
TypeScript的结构性子类型是根据JavaScript代码的典型写法来设计的。 因为JavaScript里广泛地使用匿名对象,例如函数表达式和对象字面量,所以使用结构类型系统来描述这些类型比使用名义类型系统更好。
2,比较两个函数的兼容
3,枚举的兼容性
※,枚举类型与数字类型兼容,并且数字类型与枚举类型兼容。不同枚举类型之间是不兼容的。比如,
enum Status { Ready, Waiting };
enum Color { Red, Blue, Green };
let status = Status.Ready;
status = Color.Green; // Error
※,
4,类的兼容性
※,类与对象字面量和接口差不多,但有一点不同:类有静态部分和实例部分的类型。 比较两个类类型的对象时,只有实例的成员会被比较。 静态成员和构造函数(属于类的静态部分)不在比较的范围内。
class Animal {
feet: number;
constructor(name: string, numFeet: number) { }
}
class Size {
feet: number;
constructor(numFeet: number) { }
}
let a: Animal;
let s: Size;
a = s; // OK
s = a; // OK
※,类的私有成员和受保护成员
类的私有成员和受保护成员会影响兼容性。 当检查类实例的兼容时,如果目标类型包含一个私有成员,那么源类型必须包含来自同一个类的这个私有成员。 同样地,这条规则也适用于包含受保护成员实例的类型检查。 这允许子类赋值给父类,但是不能赋值给其它有同样类型的类。
※,
5,泛型的兼容性
6,在TypeScript里,有两种兼容性:子类型和赋值。 它们的不同点在于,赋值扩展了子类型兼容性,增加了一些规则,允许和any
来回赋值,以及enum
和对应数字值之间的来回赋值。
语言里的不同地方分别使用了它们之中的机制。 实际上,类型兼容性是由赋值兼容性来控制的,即使在implements
和extends
语句也不例外。
更多信息,请参阅TypeScript语言规范.
7,
十,高级类型 (对比基础类型:string,number,数组,null,object等)
1,交叉类型(intersection types)
※,交叉类型是将多个类型合并为一个类型。 这让我们可以把现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性。 例如, Person & Serializable & Loggable
同时是 Person
和 Serializable
和Loggable
。 就是说这个类型的对象同时拥有了这三种类型的成员。
function extend<T, U>(first: T, second: U): T & U { let result = <T & U>{}; for (let id in first) { (<any>result)[id] = (<any>first)[id]; } for (let id in second) { if (!result.hasOwnProperty(id)) { (<any>result)[id] = (<any>second)[id]; } } return result; } class Person { constructor(public name: string) { } } interface Loggable { log(): void; } class ConsoleLogger implements Loggable { log() { // ... } } var jim = extend(new Person("Jim"), new ConsoleLogger()); var n = jim.name; jim.log();
※,
※,
2,联合类型 (union types)
※,
/**
* Takes a string and adds "padding" to the left.
* If 'padding' is a string, then 'padding' is appended to the left side.
* If 'padding' is a number, then that number of spaces is added to the left side.
*/
function padLeft(value: string, padding: string | number) {
// ...
}
let indentedString = padLeft("Hello world", true); // errors during compilation
联合类型表示一个值可以是几种类型之一。 我们用竖线( |
)分隔每个类型,所以 number | string | boolean
表示一个值可以是 number
, string
,或 boolean
。
如果一个值是联合类型,我们只能访问此联合类型的所有类型里共有的成员。
※,
3,
5,
十一,
1,
十二,
1,
《二》, 随记(问题及相关记录)
※,关于JavaScript的变量提升和函数提升
一,变量提升 console.log(a);//报错,a未定义 ------------------------------- console.log(a);//undefined var a=3 解析:实际执行顺序如下 var a;//变量提升,全局作用域范围内,此时只是声明,并没有赋值 console.log(a);//undefined a = 3;//此时赋值 --------------------------------- 二,函数提升 函数有两种,声明式和字面量式(匿名函数),只有声明式会提升而且优先级高于变量提升 console.log(f1);//function f1(){} var f1 = "abc"; function f1(){} console.log(f1);//abc
f1();//报错,f1不是函数
解析:实际执行顺序如下 function f1(){}//函数提升,整个代码块提升到文件最开始(和变量提升不同) console.log(f1);//function f1(){} var f1 = "abc"; console.log(f1);//abc
f1();//报错,此时f1="abc",不是函数,不能被调用。
※,JS函数的静态属性和实例属性
let A = function () { this.b= "bbbb"//b是函数A的实例属性 return ("xxx"); } A.c = "ccc";//c是函数A的静态属性 console.log(A.b);//undefined 实例属性不能用函数本身调用 console.log(A.c);//ccc 静态属性直接用函数本身调用 let a = new A(); l(a.b);//bbbb 实例属性可以在实例上调用
※