TypeScript初识——基于Java、Android开发体系

一 概要

本文不是全体系知识介绍,只是在了解TS语言的时候自己的一些对比及思考。(有些理解可能有问题,望斟酌。)
参考的文章:
link1
link2

二 对比与思考

1.TS是JS么

是它的超集(可以认为是封装的DSL),TS最终是编译程JS来工作。从这点来看,能使用TS的项目,也是能使用JS的,JS是根基。

TS是用来干什么的?
JS是用来干什么的呢。

为什么要有TS
因为JS太过于灵活,给它加上约束后编写就会更加容易。

2.作用域

const a: string = 'apple'

单独写在一个文件中,可以认为是全局静态变量,但不同的是,a是唯一的,可以认为“全局静态”这个作用域点变量是唯一的。这意味着,不能在其他的文件中也这样编码。要么:

  • 让其在function中定义,使其成为局部变量(作用域在function)
  • 文件采用export,这样文件的作用域就不是全局,而是这个“文件模块的作用域”
let、var、const

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" }

...变量的写法就是展开——>临时生成扩增变量的对象。

3.类型

原始类型(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可以。

元组类型(Tuple Types)
//定义。元——>元素类型,组——>数组,合起来就是“各种元素类型的数组”
//案例中给到两种类型,对应的赋值也是对应了类型,其类同Java中的List,只不过它更明确了数组中每个元素下标对应的类型和对应关系。
const tuple: [number, string] = [18, 'zhangsan']

//解构。既然已经知道了明确对应关系,“反向推导”——>解构出对应的值也是可以的。
const [age, name] = tuple
 
  
函数类型(Function Types)
function func (a: number, b:number = 10): string {
    return 'func1'
}

func(100)

使用过Kotlin的都知道怎么用,这是一样的。

3.类、接口、抽象类

与Java一致,修饰符的定义也是如此

1.接口

可变类型的接口、类

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类,至于如何表现需要后续斟酌。

2.类

大致和Java使用方法一致

3.函数
  1. 有名函数和匿名函数
// Named function
function add(x, y) {
    return x + y;
}

// Anonymous function
let myAdd = function(x, y) { return x + y; };

因为函数是一等公民,函数也是对象,匿名函数也是可以赋值的。这种设计简化了函数范式的结构,因此使用起来更为灵活。

  1. 可变参数
function buildName(firstName: string, ...restOfName: string[]) {
  return firstName + " " + restOfName.join(" ");
}
let employeeName = buildName("Joseph", "Samuel", "Lucas", "MacKinzie");

这点类似Java中的写法,不同之处在于可变参数的类型不用加上[]

4.类型兼容

link这里说得很清楚了,总而言之就是父集合是兼容子集合的。

5.高级类型
  1. 交叉类型(Intersection Types)
    也就是将多个类型合并成一个类型。
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();

其实也是也有疑问的,如果两个类型有同名成员且赋值不同的值,该如何区分?

  1. 联合类型(Union Types)
    交叉类型的操作符是& ,而联合类型的操作符是|。看案例:
interface Bird {
    fly();
    layEggs();
}

interface Fish {
    swim();
    layEggs();
}

function getSmallPet(): Fish | Bird {
    // ...
}

let pet = getSmallPet();
pet.layEggs(); // okay
pet.swim();    // errors

交叉类型和联合类型的区别从写法即可区分,前者是多个类型组合成了一个新的类型,后者是类型的组合,在定义方法返回参数的时候,前者是返回这个新的类型,而后者是只要满足从类型的组合中返回其中一个类型即可。

  1. 类型的判断
    根据不同的类型进行不同成员的调用。譬如上述的pet.swim就发生了错误。需要有判断类型的方法。
    方法1(泛型指定):
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用法一致。

6.其他类型
  1. 可空是undefined,不是null
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'
  1. 断言!同Kotlin用法!!

4.迭代

for..offor..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"
}

5.模块

TypeScript与ECMAScript 2015一样,任何包含顶级import或者export的文件都被当成一个模块。相反地,如果一个文件不带有顶级的import或者export声明,那么它的内容被视为全局可见的(因此对模块也是可见的)。

1.导出(export)

不仅仅是可以在当前文件进行导出,也可以在某一个文件导出多个文件。

export * from "./StringValidator"; // exports interface StringValidator
export * from "./LettersOnlyValidator"; // exports class LettersOnlyValidator
export * from "./ZipCodeValidator";  // exports class ZipCodeValidator
2.导入(import)
import { ZipCodeValidator } from "./ZipCodeValidator";

let myValidator = new ZipCodeValidator();
3.export = 和 import = require()

若使用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不能放在方法逻辑中。

4.命名空间(namespace)

“内部模块”现在叫做“命名空间”。 另外,任何使用 module关键字来声明一个内部模块的地方都应该使用namespace关键字来替换。使用命名空间是为了提供逻辑分组和避免命名冲突。

也就是module对外,namespace对内。

5.模块扩展
// 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关键词的)

6.JSX

行为类似于jsp——>前端代码里面可以写逻辑代码,JSX这里是可以嵌入TS逻辑代码

JSX是一种嵌入式的类似XML的语法。 它可以被转换成合法的JavaScript,尽管转换的语义是依据不同的实现而定的。 JSX因React框架而流行,但也存在其它的实现。 TypeScript支持内嵌,类型检查以及将JSX直接编译为JavaScript。

  1. 固有元素
declare namespace JSX {
    interface IntrinsicElements {
        foo: any
    }
}

<foo />; // 正确
<bar />; // 错误,因为它没在JSX.IntrinsicElements里指定。

因此,可以理解为在JSX.IntrinsicElements里指定的元素就是固有元素。(意义未名)
当然,也可以定义动态元素。

declare namespace JSX {
    interface IntrinsicElements {
        [elemName: string]: any;
    }
}

这种方式可以捕获所有字符串的元素。

剩余的部分文章介绍很繁琐晦涩难懂,不展开了长久以来(包括很多教科书)都是灌输式的教学方式,学习的人一开始接触了解的门槛就很高。我只想说,引导、类比式的教学会更生动、易于理解和记忆。

7.注解
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中可以不在括号中指定参数的类型,进而是可以通过注解来实现。

6.其他

  1. 有些封装类型不要使用
    Number,String,Boolean和Objectnumber,string,bool和object的封装类型。因为经常被用错,所以官网是建议不要使用封装类型,而Java中的封装类型与基本数据类型区别还是挺大的。
    既然存在,可能有其作用,只是目前还未涉及

  2. 使用可选参数、联合类型减少方法重载

/* 错误 */
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;
}
  1. TS中创建新类型的方式
  • 类型别名声明(type sn = number | string;)
  • 接口声明(interface I { x: number[]; })
  • 类声明(class C { })
  • 枚举声明(enum E { A, B, C })
  • 指向某个类型的import声明
  1. tsconfig.json
    类似Android中的gradle文件,依赖、配置、打包文件,位于根目录下。

三 总结

  • TS就是套用较多Java特性的JS语言,它具有更多类似Java语言规范的约束
  • TS的类型映射较Java更灵活
  • 尽量不要使用逻辑性需要特别推测的语法,新上手容易搞晕,尤其TS这种语法灵活的开发语言
  • 注重开发工具的提示,能了解、加深语法认识
  • 什么时候能去掉语句结尾的分号;

你可能感兴趣的:(TypeScript,android,typescript,java,android)