本文不是全体系知识介绍,只是在了解TS语言的时候自己的一些对比及思考。(有些理解可能有问题,望斟酌。)
参考的文章:
link1
link2
是它的超集(可以认为是封装的DSL),TS最终是编译程JS来工作。从这点来看,能使用TS的项目,也是能使用JS的,JS是根基。
TS是用来干什么的?
JS是用来干什么的呢。
为什么要有TS
因为JS太过于灵活,给它加上约束后编写就会更加容易。
const a: string = 'apple'
单独写在一个文件中,可以认为是全局静态变量,但不同的是,a
是唯一的,可以认为“全局静态”这个作用域点变量是唯一的。这意味着,不能在其他的文件中也这样编码。要么:
TS中使用了let
来替换var
声明变量。
为什么要用let?
因为var的作用域容易让人搞混乱,它太灵活了。使用let后,可以认作其使用方式与Java(Koltin)声明变量一致。
展开
let defaults = { food: "spicy", price: "$$", ambiance: "noisy" };
let search = { ...defaults, food: "rich" };
//search的值为{ food: "rich", price: "$$", ambiance: "noisy" }
...变量
的写法就是展开——>临时生成扩增变量的对象。
原始类型(Primitive Types)
string、number、boolean
object类型(object Types)
万物皆对象,所有非原始类型的类型。(object表示非原始类型,也就是除number,string,boolean,symbol,null或undefined之外的类型。)
// 函数
const fn: object = function () {}
// 普通对象
const obj: object = {}
// 数组
const arr: object = []
any类型
any的使用范围比object更大。
在对现有代码进行改写的时候,any类型是十分有用的,它允许你在编译时可选择地包含或移除类型检查。 你可能认为 Object有相似的作用,就像它在其它语言中那样。 但是 Object类型的变量只是允许你给它赋任意值 - 但是却不能够在它上面调用任意的方法,即便它真的有这些方法:
let notSure: any = 4;
notSure.ifItExists(); // okay, ifItExists might exist at runtime
notSure.toFixed(); // okay, toFixed exists (but the compiler doesn't check)
let prettySure: Object = 4;
prettySure.toFixed(); // Error: Property 'toFixed' doesn't exist on type 'Object'.
简而言之,object无法跳过编译器检查,any可以。
//定义。元——>元素类型,组——>数组,合起来就是“各种元素类型的数组”
//案例中给到两种类型,对应的赋值也是对应了类型,其类同Java中的List
const tuple: [number, string] = [18, 'zhangsan']
//解构。既然已经知道了明确对应关系,“反向推导”——>解构出对应的值也是可以的。
const [age, name] = tuple
function func (a: number, b:number = 10): string {
return 'func1'
}
func(100)
使用过Kotlin的都知道怎么用,这是一样的。
与Java一致,修饰符的定义也是如此
可变类型的接口、类
interface SquareConfig {
color?: string;
width?: number;
[propName: string]: any;
}
具有color/width属性的接口,其他属性可变。
interface Counter {
(start: number): string;
interval: number;
reset(): void;
}
function getCounter(): Counter {
let counter = <Counter>function (start: number) { };
counter.interval = 123;
counter.reset = function () { };
return counter;
}
let c = getCounter();
c(10);
c.reset();
c.interval = 5.0;
上面是接口的定义与实现,可以看到接口里面的变量有多种,其实现方式各有不同。
接口的示例化都需要(参数)
,使用泛型来指定类型。
实现接口仍旧是implements
继承类仍旧是extends
需要重点关注的是:接口也能extends类,至于如何表现需要后续斟酌。
大致和Java使用方法一致
// Named function
function add(x, y) {
return x + y;
}
// Anonymous function
let myAdd = function(x, y) { return x + y; };
因为函数是一等公民,函数也是对象,匿名函数也是可以赋值的。这种设计简化了函数范式的结构,因此使用起来更为灵活。
function buildName(firstName: string, ...restOfName: string[]) {
return firstName + " " + restOfName.join(" ");
}
let employeeName = buildName("Joseph", "Samuel", "Lucas", "MacKinzie");
这点类似Java中的写法,不同之处在于可变参数的类型不用加上[]
link这里说得很清楚了,总而言之就是父集合是兼容子集合的。
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();
其实也是也有疑问的,如果两个类型有同名成员且赋值不同的值,该如何区分?
&
,而联合类型的操作符是|
。看案例:interface Bird {
fly();
layEggs();
}
interface Fish {
swim();
layEggs();
}
function getSmallPet(): Fish | Bird {
// ...
}
let pet = getSmallPet();
pet.layEggs(); // okay
pet.swim(); // errors
交叉类型和联合类型的区别从写法即可区分,前者是多个类型组合成了一个新的类型,后者是类型的组合,在定义方法返回参数的时候,前者是返回这个新的类型,而后者是只要满足从类型的组合中返回其中一个类型即可。
pet.swim
就发生了错误。需要有判断类型的方法。let pet = getSmallPet();
if ((<Fish>pet).swim) {
(<Fish>pet).swim();
}
else {
(<Bird>pet).fly();
}
方法2(typeof):
function padLeft(value: string, padding: string | number) {
if (typeof padding === "number") {
return Array(padding + 1).join(" ") + value;
}
if (typeof padding === "string") {
return padding + value;
}
throw new Error(`Expected string or number, got '${padding}'.`);
}
typeof的方法是用来比较字符串,=是否只能用于基本数据类型(自定义类型不可)?=
方法3(instanceof):
与Java用法一致。
class C {
a: number;
b?: number;
}
let c = new C();
c.a = 12;
c.a = undefined; // error, 'undefined' is not assignable to 'number'
c.b = 13;
c.b = undefined; // ok
c.b = null; // error, 'null' is not assignable to 'number | undefined'
!
同Kotlin用法!!
for..of
、for..in
let someArray = [1, "string", false];
for (let entry of someArray) {
console.log(entry); // 1, "string", false
}
/
let list = [4, 5, 6];
for (let i in list) {
console.log(i); // "0", "1", "2",
}
for (let i of list) {
console.log(i); // "4", "5", "6"
}
TypeScript与ECMAScript 2015一样,任何包含顶级import或者export的文件都被当成一个模块。相反地,如果一个文件不带有顶级的import或者export声明,那么它的内容被视为全局可见的(因此对模块也是可见的)。
不仅仅是可以在当前文件进行导出,也可以在某一个文件导出多个文件。
export * from "./StringValidator"; // exports interface StringValidator
export * from "./LettersOnlyValidator"; // exports class LettersOnlyValidator
export * from "./ZipCodeValidator"; // exports class ZipCodeValidator
import { ZipCodeValidator } from "./ZipCodeValidator";
let myValidator = new ZipCodeValidator();
若使用export =导出一个模块,则必须使用TypeScript的特定语法import module = require(“module”)来导入此模块。
let numberRegexp = /^[0-9]+$/;
class ZipCodeValidator {
isAcceptable(s: string) {
return s.length === 5 && numberRegexp.test(s);
}
}
export = ZipCodeValidator;
import zip = require("./ZipCodeValidator");
// Validators to use
let validator = new zip();
require
的作用是动态引入,区别于import
不能放在方法逻辑中。
“内部模块”现在叫做“命名空间”。 另外,任何使用 module关键字来声明一个内部模块的地方都应该使用namespace关键字来替换。使用命名空间是为了提供逻辑分组和避免命名冲突。
也就是module
对外,namespace
对内。
// observable.js
export class Observable<T> {
// ... implementation left as an exercise for the reader ...
}
// map.js
import { Observable } from "./observable";
Observable.prototype.map = function (f) {
// ... another exercise for the reader
}
可以看到上述Observable类其实是没有map成员的,如果要动态声明这个成员该如何做?
// observable.ts stays the same
// map.ts
import { Observable } from "./observable";
declare module "./observable" {
interface Observable<T> {
map<U>(f: (x: T) => U): Observable<U>;
}
}
Observable.prototype.map = function (f) {
// ... another exercise for the reader
}
// consumer.ts
import { Observable } from "./observable";
import "./map";
let o: Observable<number>;
o.map(x => x.toFixed());
也可以实现全局扩展
// observable.ts
export class Observable<T> {
// ... still no implementation ...
}
declare global {
interface Array<T> {
toObservable(): Observable<T>;
}
}
Array.prototype.toObservable = function () {
// ...
}
可以看到通过declare
再次声明定义这个成员接口。(类似于kotlin中的扩展函数的使用方法,只不过这里是需要家还是那个declare
关键词的)
行为类似于jsp——>前端代码里面可以写逻辑代码,JSX这里是可以嵌入TS逻辑代码
JSX是一种嵌入式的类似XML的语法。 它可以被转换成合法的JavaScript,尽管转换的语义是依据不同的实现而定的。 JSX因React框架而流行,但也存在其它的实现。 TypeScript支持内嵌,类型检查以及将JSX直接编译为JavaScript。
declare namespace JSX {
interface IntrinsicElements {
foo: any
}
}
<foo />; // 正确
<bar />; // 错误,因为它没在JSX.IntrinsicElements里指定。
因此,可以理解为在JSX.IntrinsicElements里指定
的元素就是固有元素。(意义未名)
当然,也可以定义动态元素。
declare namespace JSX {
interface IntrinsicElements {
[elemName: string]: any;
}
}
这种方式可以捕获所有字符串的元素。
剩余的部分文章介绍很繁琐晦涩难懂,不展开了长久以来(包括很多教科书)都是灌输式的教学方式,学习的人一开始接触了解的门槛就很高。我只想说,引导、类比式的教学会更生动、易于理解和记忆。
class C {
constructor() {
/** @type {number | undefined} */
this.prop = undefined;
/** @type {number | undefined} */
this.count;
}
}
let c = new C();
c.prop = 0; // OK
c.count = "string"; // Error: string is not assignable to number|undefined
要注意的是/** */
并不是注释,这点与Java不同,简单的文档提示语句。
@type {number | undefined}
这个是注解,限定类型为{number | undefined}
注解在这里被当成了很重要的一种限定方式。因为ts中可以不在括号中指定参数的类型,进而是可以通过注解来实现。
有些封装类型不要使用
Number,String,Boolean和Object
是number,string,bool和object
的封装类型。因为经常被用错,所以官网是建议不要使用封装类型,而Java中的封装类型与基本数据类型区别还是挺大的。
既然存在,可能有其作用,只是目前还未涉及
使用可选参数、联合类型减少方法重载
/* 错误 */
interface Example {
diff(one: string): number;
diff(one: string, two: string): number;
diff(one: string, two: string, three: boolean): number;
}
/* OK */
interface Example {
diff(one: string, two?: string, three?: boolean): number;
}
方法当然是越少越好。kotlin也是类似的思想,不过是要在参数中指定具体的值。
/* WRONG */
interface Moment {
utcOffset(b: number): Moment;
utcOffset(b: string): Moment;
}
/* OK */
interface Moment {
utcOffset(b: number|string): Moment;
}
;