从历史的角度入门TypeScript

3,227 阅读13分钟

引言

如果你了解 npmtrends.com,你可以在上面轻松获取以下这个截图信息。

图示 TypeScript 的下载量在 5 年内呈现指数级增长。如果咱按照买基金股票抄底思路,学习 TypeScript 的最佳时间或许就是几年前。

但是谁都无法回到过去,别灰心,还有另一个最佳时间入手 TS,那便是现在。

若你是 VUE 技能拥有者,则更是不容错过,很多项目都正在或已在使用 TypeScript 进行重构。VUE3 + TS! 未来已来!

小手一赞👍,月入百万💴

为什么是 TypeScript

Why JavaScript?

在解释为什么是 TypeScript 之前,我们先回顾下为什么是 JavaScript。对此节已经烂熟于心的铁子可选择速读或跳过。

  • 命名史

时间回到 1994 年年底,Netscape(网景) 公司推出了一款全新的网页浏览器 —— Netscape Navigator,点燃了整个互联网【1】。公司高层有预见性的设想:网络将更动态【2】,于是在 1995 年招募了 Brendan Eich(布兰登·艾克) 为这款全新的浏览器设计一门胶水语言,用于:方便网页开发者组装图片或插件之类的组件。

1995 年 5 月,这位老哥十天就把原型设计出来了,最初命名为 Mocha。同年 9 月,在 Netscape Navigator 2.0 的 Beta 版中改名为LiveScript。同年 12 月,在 Netscape Navigator 2.0 Beta 3 中部署时被重命名为JavaScript。主要是为了蹭一下 Java 这个编程语言热词【3】

弦外之音:

【1】:90 年代中期,网景浏览器的市场占有率一度高达 90% ,本瓜无法想象这是怎样的一个占有率。可能意味着一句话吧:前端不用做兼容。( 2020年,谷歌浏览器的全球市场占有率也才百分之六十几)

【2】:网景公司创造定义了浏览器的一系列标准和技术,除了 JavaScript,还包括著名的 http cookies。牛 P! 即使强如斯,最终也在浏览器大战中败北。幸得留下 Mozilla Firefox 这枚火种。

【3】:JavaScript 和 Java 没有关系。不愧是你!蹭热度的基因早已写在了每个前端银儿的基因里!

  • ECMAScript

1996 年 11 月,网景公司将 JavaScript 提交给 Ecma International(欧洲计算机制造商协会)进行标准化【1】。新的语言标准取名为 ECMAScript

从 1997 年开始,ECMAScript 几乎上是一年一版,除了 1999 年至 2009 年这十年间只发布了一版激进的 ECMAScript 4。

在这“丢失的十年”间,各大浏览器厂商各种造规则,导致 ECMAScript 逐渐没落。直到 Ajax 时代来临,ECMAScript 又被提上了日程。

2015 年,代号为 Harmony(和谐) 的 ES6 诞生啦【2】

一切似乎都将迎来大一统。

然而,事与愿违,市面上各浏览器对于 ECMAScript 新特性的支持又不尽相同,导致我们在生产环境仍需要进行编译转化,例如:用 Babel 将 ES6 转成 ES5......【3】

弦外之音:

【1】:JavaScript 的诞生吸引了微软,于是微软在 IE 3.0 上搭载了 JScript,JScript 其实也是 JavaScript 的一种实现。为了避免语言标准的分裂,网景公司提交了规范。(点赞PEACE)

【2】:到 2020 年,ECMAScript 已经发布第 11 版了!ES6 你 精通了嘛?

【3】:本瓜疑思:如果当初网景公司微软公司选择合作而非对拼,共同打造一款浏览器和一门脚本语言,现在的浏览器环境是不是会更好?至少会不那么混乱吧!但是或许混乱也代表着公平,标准也意味着代表垄断。欢迎讨论~

Why TypeScript?

随着 Ajax 的火热和 JavaScript 的复兴,大型 Web 应用越来越多,JS 的问题也逐渐被暴露。

微软的语言开发者发现:JavaScript 仅仅是一门脚本语言,其设计理念简单,缺乏对类与模块的支持,不适合用于开发大型的 Web 应用。

于是,2012 年 10 月,Delphi、C# 之父Anders Hejlsberg(安德斯·海尔斯伯格) 发布了 TypeScript

它的特点如下:

  1. 基于 ECMAScript 标准进行拓展,是 JavaScript 的严格超集。
  2. 添加了可选静态类型、类和模块。
  3. 可以编译成 JavaScript ,也可以和 JavaScript 一起运行。
  4. 编译时检查,但不污染运行时。

TS 是 JS 严格的化身。

静态类型、动态类型、弱类型、强类型

你是否困惑,为什么计算机也有这么多语言。其实它们也是可以按照某些标准去分类的,比如:静态类型、动态类型、弱类型、强类型。

一图胜千言:

本瓜简单制表一张:

名称特性
静态类型编译时做类型检查
动态类型运行时做类型检查
强类型不允许变量类型的隐式转换
弱类型允许变量类型的隐式转换

这里抛一个小问题: 你认为 TypeScript 是强类型语言吗?

  • Flow 工具

在这里顺带提一下Flow。Flow 是 Facebook 在 2014 年发布的一个类型检查工具,用来检查 React 的源码。它也经常被拿出来和 TS 作比较。

相比较于 Flow,TypeScript 作为一门完整的编程语言,它的功能更为强大。生态也更健全、更完善。特别是对于开发工具这一块(背靠微软)。

时间可以检验一切,从现在的角度回看二者,我想至少可以得出这样的结论:TypeScript 更受欢迎。

环境配置 && Hello World

说了这么多,你的大刀是否早已饥渴难耐了?

让我们回到那最初的起点:Hello World!

  1. Node 环境。

安装Node环境勿需多言。

  1. 安装 typescript。
npm install -g typescript
  1. tsc 命令用于将 .ts 编译成 .js。
tsc hello.ts

# hello.ts => hello.js
  1. 安装 ts-node 可直接在 .js 里运行 ts 代码。
npm install -g ts-node
  1. 编译运行。
ts-node index.js
  1. 展示。

类型 & 函数

类型

类型是计算机语言的基础之一,就好比汉语的声母、韵母,英语的主系表、定状补。

BooleanNumberStringArrayNullUndefined,默认你知道!

还有?关键的来了:

Enum(枚举)Any (任意)Unknown(未知)Tuple(元组)void(与 Any 相反)Never(用于不存在)

下面列代码一二、可自作感受。

  • Boolean
let isDone: boolean = false;
// ES5:var isDone = false;
  • Number
let count: number = 10;
// ES5:var count = 10;
  • String
let name: string = "Semliker";
// ES5:var name = 'Semlinker';
  • Array
let list: number[] = [1, 2, 3];
// ES5:var list = [1,2,3];

let list: Array<number> = [1, 2, 3]; // Array<number>泛型语法
// ES5:var list = [1,2,3];
  • undefined
let u: undefined = undefined;
  • null
let n: null = null;
  • Enum(枚举)

TypeScript 支持数字的和基于字符串的枚举。

// 数字:从 0 开始计数
enum EnumNum {
  a,
  b,
  c,
  d,
}

// 字符串
enum EnumStr {
  a = "a",
  b = "b",
  c = "c",
  d = "d",
}

// 数字加字符串(异构)
enum EnumNumAndStr {
  a,
  b,
  c = "c",
  d = 3,
  e,
}
  • Any(任意)

在 TS 中,任何类型都可以被归为 any 类型。这让 any 类型成为了类型系统的顶级类型(也被称作全局超级类型)

let value: any;

value.foo.bar; // OK
value.trim(); // OK
value(); // OK
new value(); // OK
value[0][1]; // OK

使用 any 类型,可以很容易地编写类型正确但在运行时有问题的代码。如果我们使用 any 类型,就无法使用 TypeScript 提供的大量的保护机制。为了解决 any 带来的问题,TypeScript 3.0 引入了 unknown 类型。

  • Unknown(未知)

Unknown 成为 TypeScript 类型系统的另一种顶级类型。

Unknown 类型只能被赋值给 any 类型和 unknown 类型本身。

let value: unknown;

value = true; // OK
value = 42; // OK
value = {}; // OK

let value1: unknown = value; // OK
let value2: any = value; // OK
let value3: boolean = value; // Error
let value4: number = value; // Error
let value6: object = value; // Error
let value7: any[] = value; // Error

理解:未知的变量可以是任一一个(赋值给 unknown),但是又不等于任一具体的(将 unknown 赋值给其他值)。

  • Tuple(元组)

元组可用于定义具有有限数量的未命名属性的类型。每个属性都有一个关联的类型。使用元组时,必须提供每个属性的值。为了更直观地理解元组的概念。

let tupleType: [string, boolean];
tupleType = ["Semlinker", true];
  • void(与 Any 相反)

void 类型像是与 any 类型相反,它表示没有任何类型。当一个函数没有返回值时,你通常会见到其返回值类型是 void:

需要注意的是,声明一个 void 类型的变量没有什么作用,因为它的值只能为 undefined 或 null:

  • never(用于不存在)

never 类型表示的是那些永不存在的值的类型。 例如,never 类型是那些总是会抛出异常或根本就不会有返回值的函数表达式或箭头函数表达式的返回值类型。

函数

接下来康康 TS 和 JS 的函数有何区别?

重点区别有:函数类型是否可选参数函数重载

  • 函数类型
// 参数含有类型、返回类型即函数类型

let IdGenerator: (chars: string, nums: number) => string;

function createUserId(name: string, id: number): string {
  return name + id;
}

IdGenerator = createUserId;
  • 可选参数
// 可以通过 ? 号来定义可选参数
function createUserId(name: string, id: number, age?: number): string {
  return name + id;
}
  • 函数重载或方法重载是使用相同名称和不同参数数量或类型创建多个方法的一种能力。

通俗来讲就是:根据传递的参数不同,会有不同的表现形式。

// 函数的重载
function getInfo(name:string):void;
function getInfo(age:number):void;
function getInfo(str:any):void{
    if(typeof str == "string"){
        console.log("名字:",str)
    }
    if(typeof str == "number"){
        console.log("年龄",str)
    }
}
getInfo("zhangsan")

不仅函数可以重载,方法也能重载,思想就是:按照重载表依次执行直到合法的那次,更多可自行探索。

接口 & 类

接口

接口是一个很重要的概念,进行过前后端联调的都知道,对接接口就是实现功能最重要的部分。现在前端敏捷开发更多是组件复用,接口复用或许还是一个方向。

TypeScript 接口除了用于对类的一部分行为进行抽象表示以外,也常用于对「对象的形状(Shape)」进行描述。

  • 典型示例
interface Shape {
    head: string;
    arm: string;
}
interface Human {
    name: string;
    age: number;
    shape: Shape;
    say(word: string): void;
}

let jack: Human = {
    name: 'Jack',
    age: 18,
    shape: {
        head: 'head',
        arm: 'arm'
    },
    say(word: string) {
        console.log(word)
    }
}
jack.say('hi')

  • 函数接口
interface Fn {
    (a: number, b: number): number;
}

let add: Fn = function(a: number, b: number): number {
    return a + b
}

console.log(add(1, 2))
  • 接口的继承
interface Animal {
    move(): void;
}

interface Human extends Animal {
    name: string;
    age: number;
}

let jack: Human = {
    age: 18,
    name: 'Jack',
    move() {
        console.log('move')
    }
}

类是一种面向对象计算机编程语言的构造,是创建对象的蓝图,描述了所创建的对象共同的属性和方法。

js 没有严格意义上的类,ES6 的 class 实现的继承不过也是基于原型链的语法糖。有兴趣可解底层实现。在 ts 中,其实也同样如此。有兴趣阅读 TypeScript与ES6中 - 类(Class)的区别

这里直接贴下结论:

两个重点:抽象类访问限定符(接口继承上文已说明)

  • 抽象类

我们都知道 JAVA 是典型的面向对象,有四大特性:抽象、封装、继承、多态,也有很大一部分人在学习 TS 的过程中都认为它和 JAVA 很像,其中很重要的一点是因为 TS 也有抽象类。

在面向对象的概念中,所有的对象都是通过类来描绘的,但是反过来,并不是所有的类都是用来描绘对象的,如果一个类中没有包含足够的信息来描绘一个具体的对象,这样的类就是抽象类。

推荐阅读:TypeScript中的面向对象编程

在 TypeScrupt 中表示抽象类用 abstract 修饰。

abstract class Animal{
    public name:string;
    constructor(name:string){
        this.name=name;
    }

    abstract eat():any; //抽象方法 ,不包含具体实现,要求子类中必须实现此方法,否则编译报错
 
    run(){
        console.log('非抽象方法,无需要求子类实现、重写');
    }
  • 访问限定符
class Dog {
    constructor(name : string) {
        this.name = name;
    }
    public name : string;//修饰符 公有成员,对所有人都是可见的

    private pri(){};//修饰符 私有成员 ,只能在类的本身调用,不能做类的实例和子类调用
    
    protected pro() {};//修饰符 保护成员, 只能在类和子类中访问,不能做实例中访问

    readonly  legs :number = 4; //修饰符 只读属性,跟类的实例属性一样,必须初始化,

    static food : string = "bones";//修饰符 静态成员,只能通过类名访问,类的静态成员可以可以被继承

    run (){}
}

命名空间 & 模块

命名空间

命名空间定义了标识符的可见范围,一个标识符可在多个名字空间中定义,它在不同名字空间中的含义是互不相干的。我们可将相似功能的函数、类、接口等放置到命名空间内。如果想在命名空间外访问,则用export导出。

我们用关键字namespace来声明命名空间,

  • namespace
// IShape.ts
namespace Drawing { 
    export interface IShape { 
        // 如果我们需要在外部可以调用其中的类和接口,则需要在类和接口添加 export 关键字。
        draw(); 
    }
}
// Circle.ts
/// <reference path = "IShape.ts" /> 
namespace Drawing { 
    export class Circle implements IShape { 
        public draw() { 
            console.log("Circle is drawn"); 
        }  
    }
}

如果一个命名空间在一个单独的 TypeScript 文件中,则应使用三斜杠 /// 引用它,语法格式如下:

/// <reference path = "SomeFileName.ts" />
  • 嵌套命名空间
// 示例
namespace namespace_name1 { 
    export namespace namespace_name2 {
        export class class_name {    } 
    } 
}

模块

TypeScript和ECMAScript2015一样,任何包含顶级import或者export的文件都被当成一个模块。其实,也不一定是顶级。以上就是对什么叫模块的简单认知。 —— 官方文档

  //导出接口
  export interface StringValidator {
    isAcceptable(s: string): boolean;
  }

  //导出变量
  export const numberRegexp = /^[0-9]+$/;

  //导出类
  export class ZipCodeValidator implements StringValidator
  {
    isAcceptable(s: string) {
      return s.length === 5 && numberRegexp.test(s)
    }
  }

  //另一种导出方式
  export {StringValidator, numberRegexp, ZipCodeValidator }

  //假如在一个模块里,只导出一个类或函数,那么最佳实践就是用默认导出语法

  export default function Test (s: string) : boolean {
    return s.length === 5 
  }

  // 导入方法一
  import{ StringValidator, numberRegexp, ZipCodeValidator } from "./ZipCodeValidator"

  // 导入方法二
  import * as validator from "./ZipCodeValidator"

  let zip = new validator.ZipCodeValidator()

  //如果是默认导出的话,可以直接这么导入
  import test from "../../xxx.js"

和 es6 一致。

  • 最佳实践

在模块中,最佳实践是导出一个新的实体来提供新的功能(即模块扩展)。

//Calculator.ts
  export class Calculator {
    private current = 0;
    private memery = 0;
    private operator: string;

    //charCodeAt() 返回指定位置的字符编码
    processDigit(digit: string, currentValue: number){
      if(digit >= "0" && digit <= "9"){
        return currentValue * 10 + (digit.charCodeAt(0) - "0".charCodeAt(0))
      }
    }
  }

  //现在来扩展它
  import { Calculator } from "./Calculator"

  class ProgrammerCalculator extends Calculator {
    static digits = ["0", "1", "2", "3", "4"];
    constructor(public base: number){
      super()
      if(base <= 0 || base > ProgrammerCalculator.digits.length){
        throw new Error ("base has to be within 0 to 16 inclusive")
      }
    }
    protected processDigit(digit: string, currentValue: number){
      if(ProgrammerCalculator.digits.indexOf(digit) >= 0 ) { // 存在 digit
        return currentValue * this.base + ProgrammerCalculator.digits.indexOf(digit) // 自定义的扩展计算
      }
    }
  }

  //导出
  export { ProgrammerCalculator as Calculator }

  //在模块中不要使用命名空间

如何应用到 Vue 项目?

TS 在 Vue 项目中的使用基于装饰器,它为我们在类的声明及成员上通过元编程语法添加标注提供了一种方式。Vue 装饰器写法

  1. 修改入口文件main.js为main.ts,修改webpack.config.js
const path = require('path')
module.exports = {
  ...
  pages: {
    index: {
      entry: path.resolve(__dirname+'/src/main.ts')
    },
  },
  ...
}
  1. 添加装饰器
npm install vue-class-component vue-property-decorator -D
  1. 改造 vue
<script lang="ts">
import { Component, Vue, Prop, Watch  } from "vue-property-decorator";
import draggable from 'vuedraggable'

@Component({
  created(){
    
  },
  components:{
    draggable
  }
})
export default class MyComponent extends Vue {
  /* data */
  private ButtonGroup:Array<any> = ['edit', 'del']
  public dialogFormVisible:boolean = false
  
  /*method*/
  setDialogFormVisible(){
    this.dialogFormVisible = false
  }
  addButton(btn:string){
    this.ButtonGroup.push(btn)
  }

  /*computed*/
  get routeType(){
    return this.$route.params.type
  }
  
  @Prop({type: Number,default: 0}) readonly id!: number // 父传子的prop,不能子组件不能修改

  @Watch('dialogFormVisible')
  dialogFormVisibleChange(newVal:boolean){
    // 一些操作
  }
}
</script>

更多装饰器的使用查看vue-property-decorator

小结

这是一篇入门级 TypeScript 学习导读,虽然不尽完备,但整体上的重点都已点出说明,并贴出关键代码(Talk is cheap)。如果文首从历史的角度切入学习能吸引你的兴趣,那真是太棒了。

本瓜对 TypeScript 的理解是:由于前端的发展壮大,JavaScript 作为一门脚本语言已经无法支撑大型应用的规范开发。借鉴于开发大型应用的成功典范 Java 的面向对象思想(抽象、封装、继承、多态),TS 严格定义了不同的类型、规范了传参,实现了抽象类,实现了接口继承、优化模块化思想等。

能够想象,有了 TS 的前端项目的协同开发,代码会有更高的复用性、可读性、易维护性,生产效率也会更高。

生产力决定财富!早学早享受!愿你我都能与美妙的代码相伴!

参考文档