万物起源-从 JavaScript 到 TypeScript

万物起源-从 JavaScript 到 TypeScript

万物起源

Tim Berners-Lee 于1989年发明了万维网并于1991年对外发布了世界上第一个网页浏览器 WorldWideWeb,从此拉开了 Web 时代的序幕;

1994 年网景公司(Netscape)发布了 Navigator 浏览器 0.9 版本,这是历史上第一个比较成熟的网络浏览器,轰动一时。

彼时这个版本的浏览器只能用来浏览,不具备与访问者互动的能力,因此 Netscape 急需一种网页脚本语言,使浏览器可以与网页进行互动。1995年5月 Sun 公司推出了 Java 语言,这是一种 Write Once Run Anywhere 的编程语言,它看上去在未来会非常成功。

同年的夏天 Netscape 雇佣了 Brendan Eich,目标是把Scheme语言嵌入到 Netscape Navigator浏览器当中,但更早之前 Netscape 已经跟 Sun 公司合作在 Netscape Navigator 中支持 Java,彼时 Netscape 内部也产生了激烈的争论,最后 Netscape 决定发明一种可以和 Java 搭配使用的脚本语言。

为了应付公司对网页脚本语言的任务,Brendan Eich 只用了十天时间就把 JavaScript 设计了出来。最初这个语言被命名为 Mocha,1995年9月在 Netscape Navigator 2.0 的 Beta 版中改名为 LiveScript,同年12月,Netscape Navigator 2.0 Beta 3中部署时被重命名为 JavaScript。当时 Netscape 和 Sun 公司组成的开发联盟为了能让这个语言搭上 Java 的热度,被临时命名为 JavaScript ,这也是日后成为大众对这门语言有诸多误解的原因之一。

谁也想不到二十年后的今天 JavaScript 似乎印证了 Write Once Run Anywhere 的优势,Brendan Eich 作为 JavaScript 之父,在编程的历史上留下了他的印记。

JavaScript 的历史

1998年后 Internet Explorer 取代 Netscape Navigator 成为主要浏览器。

黑暗时刻

Netscape 推出 JavaScript 后在浏览器上大获成功,于是微软模仿 JavaScript 开发了一种相近的语言内置在 Internet Explorer 中并取名为 JScript,与当时处于市场领导地位的 Netscape 同台激烈竞争。由于 JScript 也是一种 JavaScript 实现,这两个语言版本在浏览器上共存也意味着语言标准化的缺失,除此之外,微软也在网页技术上加入了不少专属对象,使不少网页使用非微软平台及浏览器无法正常显示。

这一时期,网页开发有如下几个特点:

  • 使用专属格式,不尊重网页标准
  • 网页开发兼容性堪忧
  • 专属对象过多

对于网页开发者来说彼时堪称“黑暗时刻”,JavaScript 这门弱小的语言堪比工程玩具,各个方面都极度不完善,不仅如此由于浏览器大战微软一家独大的结局为开发者带来了噩梦般的兼容体验。

五年后 Internet Explorer 6 SP1 成为了最后一款可独立下载的 IE 浏览器,以这个时间点为标志宣告了浏览器大战的结束,也是同年开始 Internet Explorer 市场逐步被其他浏览器蚕食,如:Mozilla Firefox Opera Safari Chrome。时间进入2003年之后,鉴于微软产品的安全性成为人们的热门话题以及受到了 Firefox 的强力竞争,2005年最后一个季度 Internet Explorer 市场占有率随之下降到了85%。在此后的几年中网页开发者不得不面对不同浏览器的兼容性问题,为了修补这些问题,除了成立 WHATWG 工作组外,很多开发者为此开发了不同的库来解决这些问题。

参考资料:

标准化

由于 Netscape 遭遇微软的强力竞争不得不将 JavaScript 提交给国际标准化组织 ECMA,希望 JavaScript 能够成为国际标准,以此对抗微软,TC39(Technical Committee 39)负责制定和审核这个标准。1997年7月 ECMA 组织发布了262号标准文件的第一版,规定了浏览器脚本语言的标准,并将这种语言称为 ECMAScript,但由于微软 Internet Explorer 的 JScript 随着市场占有率成为了事实标准,那一段时间 ECMA 公开的标准相对弱小,直到随着 Firefox 市场占有率不断提高,Brendan 迫使微软回到了标准指定的议程中(IE9之后你再也看不见JScript的原因),目前流行最广,影响最大的一个基于 ECMAScript 标准实现的语言无疑就是 JavaScript 了。

参考资料:

什么是 TC39

前面我们讲了 TC39 负责制定和审核 ECMAScript 标准,接下来我们来看一看 TC39 都做了哪些事情。

TC39(Technical Committee 39)是一个推动 JavaScript 发展的委员会,它的成员包括了各浏览器厂商以及业务与浏览器紧密相连的公司,在标准到落地这个相对漫长的过程里,TC39 制定了一系列的规范来标准化这个流程,它一共有五个阶段:

  • stage 0 strawman:任何讨论、想法、改变或者还没加到提案的特性都在这个阶段,但只有 TC39 成员可以提交。
  • stage 1 proposal:产出一个正式的提案,发现潜在的问题,例如与其他特性的关系,实现难题,提案包括详细的API描述,使用例子,以及关于相关的语义和算法。
  • stage 2 draft:提供一个初始的草案规范,与最终标准中包含的特性不会有太大差别,草案之后,原则上只接受增量修改,开始实验如何实现,实现形式包括polyfill, 实现引擎(提供草案执行本地支持),或者编译转换(例如babel)。
  • stage 3 candidate:候选阶段,获得具体实现和用户的反馈,此后,只有在实现和使用过程中出现了重大问题才会修改,规范文档必须是完整的,评审人和ECMAScript的编辑要在规范上签字,至少要在一个浏览器中实现,提供polyfill或者babel插件。
  • stage 4 finished:已经准备就绪,该特性会出现在下个版本的ECMAScript规范之中,需要通过有2个独立的实现并通过验收测试,以获取使用过程中的重要实践经验。

资料参考:

babel 带来的价值

babel 是一个用于 Web 开发的 JavaScript 编译器,在 TC39 的标准进程中从 stage 2 draft 到 stage 3 candidate 都需求可被 babel 转换或提供 babel 插件(可选),事实上 babel 对于促进新的标准被广大开发者所使用,反馈,是极好的一种方式,TC39 0-3 我们都可以使用对应的 presets 来运用,如:

npm install --save-dev @babel/preset-stage-0

.babelrc

{
  "presets": ["@babel/preset-stage-0"]
}

资料参考:

未来

有时候令人费解的版本就像 JavaScript 混乱的出生到成长的过程,不同的年代有些版本有时根据迭代版本号命名,有时又根据年份来命名,而这种命名的不确定又使得人们更加容易混淆 JavaScript/ECMAScript 这两个概念,因此官方对于这种约束有了一种更直接的命名方式,那就是以年份为版本号,这一张图代表了 JavaScript 的发展趋势:



近些年来 JavaScript 取得的成绩大家有目共睹,作为目前唯一可运行在浏览器中的语言,以及它在其他领域如:Node.js 移动端等如火如荼的进行中,这值得你投入时间。

TypeScript 介绍

TypeScript is a typed superset of JavaScript that compiles to plain JavaScript. Any browser. Any host. Any OS. Open source.

什么是 TypeScript

微软发布了一款 JavaScript 超集的编程语言并取名为 TypeScript,由于 TypeScript 是 JavaScript 的严格超集,因此任何 JavaScript 都是合法的 TypeScript(非常像 C 和 Objective-C 的关系)。TypeScript 为 JavaScript 带来了强大的类型系统和对 ES2015的支持,它的编译工具可以运行在任何服务器和任务系统上。

事实上 ES2015 发布之后 JavaScript 取得了巨大的进步,但随着设备性能的提升以及 JavaScript 在应用层上不断占据了重要的位置,对于大型项目,人们显然需要更强大的语言级别的支持(微软发现外部客户在开发大规模 JavaScript 应用的过程中遭遇的语言本身的短板)。

类型系统实际上是非常好的文档,增强了编辑器在 智能提示跳转定义代码补全 等方向上的功能,并且在编译阶段能发现大部分的错误,对于大型工程的代码可读性和可维护性起到了了不起的作用。

TypeScript 的流行趋势

事实上 TypeScript 拥有活跃的社区,大部分第三方库都有提供 TypeScript 类型定义文件,甚至知名的前端库都完全使用 TypeScript 来进行开发,比如 Google 的 Angular,我们可以通过一些数据来了解 TypeScript 的流行趋势:

TypeScript 的优势和收益是什么

TypeScript 官网上列了很多它的优势,在这里我更愿意自己总结一下:

  • 类型系统可在编译阶段发现大部分的错误
  • 类型系统也是一个很直观的编程文档,可以查看任何函数或API的输入输出类型
  • 类型系统增强了编辑器或IDE的功能
  • TypeScript 可以自动的推导类型
  • 一切 JavaScript 都是合法的 TypeScript 降低了使用成本
  • TypeScript 拥抱 ES2015 以及 ESNext 草案规范
  • 几乎第三方库都有 TypeScript 类型定义文件

rollbar 于 2018年统计了前端项目 Top10 的错误:rollbar.com/blog/top-10,事实上 TypeScript 在编译期的类型检查能解决上述80%的问题,对于大型工程来说收益是非常明显的。

如果你有一个需要长期维护的工程,那么类型系统在可读性和可维护性上拥有比 JavaScript 更强大的动能,在良好的编程语境下,在稳定的工具链帮助下,TypeScript 可以说是目前唯一较好的选择。

当然,凡事都有两面性,TypeScript 有一定的学习成本,比如:Interfaces,Generics,Enums 等前端工程师不是很熟悉的概念,短期内多少会增加一些开发成本,集成和构建一些库会有一定的工作量,比如我们用 React 来开发一个前端工程,那么你就需要进行一些配置,当然你也可以直接使用 create-react-app 来创建一个 TypeScript + React 工程。

TypeScript 与 JavaScript 对比表格

| 对比项目 | TypeScript | JavaScript | 注意 | 
| --------| --- | --- | --- | 
| 基本类型 |  boolean number string Array Tuple Enum any void null undefined never object | string number boolean null undefined symbol | TypeScript 中 object 表示的是不是 JavaScript 基本类型的类型| 
| 变量声明 | let const var| let const var | 基本一致 | 
| 接口 | interface | 无 |  TypeScript 的核心是类型检查,因此接口充当了命名这些类型的角色 | 
| 类 | class abstract class  readonly … | class 无 abstract class | 基本一致,但不同的可能发生在未来,TypeScript 使用 private 来定义私有,而 JavaScript 未来极有可能将 #.xx 来定义私有写入标准。TypeScript  支持抽象类,只读等等。| 
| 函数 | N | N | 基本一致,参数赋默认值,剩余参数等等,唯一不同的是 TypeScript 支持?可选参数 | 
| 泛型 | Generics | 无 | 泛型是一个特别灵活的可重用指定不同类型来控制具体类型的类型,TypeScript 支持 | 
| 枚举 | Enums | 无 | TypeScript 支持的枚举不仅可以默认从 0 开始,也可以赋值具体的字符串,它的操作空间非常大 | 
| 类型推断 | 支持 | 无 |  let x = 3; TypeScript 可以通过 3 来推断 x 的类型是 number | 
| 高阶类型 | & typeof  instanceof … | 无 | TypeScript 独有 | 
| Symbols | N | N | Symbol 一样 | 
| 迭代器 | N | N | 如果实现了 Symbol.iterator ,那么就被视为可迭代的,术语上和 JavaScript 定义的一样 | 
| Generators | N | N | 一样 |
| 模块系统 | N | export import | 事实上 TypeScript 支持多种多样的模块系统,既有 ESModule 也有 Commonjs 规范,甚至还有 AMD UMD 等 |
| 其他 | N | N | 由于 TypeScript 是 JavaScript 的超集,因此 ES2016 之后以及 ESNext 定义的  api 都可以直接在 TypeScript 中使用 并不需要语言支持,至于其他一些比如 JSX Mixins 等等,由于这些不属于 JavaScript 标准因此这里不再复述 。|

环境配置

TypeScript 3.3

既然上文我们了解到 TypeScript 需要编译,那么我们肯定会使用到编译工具,因此在我们开始正式学习 TypeScript 之前需要安装一下编译环境。

  • 安装 Node.js 10.15.3 LTS
  • 安装 typescript
$ npm install -g typescript
$ tsc --version

国际惯例 Hello World

创建一个 helloworld.ts 文件,然后输入:

let helloworld = "";

console.log(helloworld);
$ tsc helloworld.ts

现在我们稍微改动一行代码,在 JavaScript 中我们完全可以如此:

let helloworld = "";

helloworld = 1;

对于语法而言这完全是正确的,但对于语句来说,不能锁定类型在某些情况下,很容易出现未知异常的问题,现在我们执行 tsc 来编译它:

$ tsc helloworld.ts
helloworld.ts:5:1 - error TS2322: Type '1' is not assignable to type 'string'.

5 helloworld = 1;
  ~~~~~~~~~~

Found 1 error.

编译器可以明确的告知你不能将 1 赋值给字符串类型,这就是 TypeScript 带来的魅力。

tsconfig.json

tsconfig.json 文件可以指定编译 TypeScript 项目所需的根目录以及编译器选项,如果你的工程中存在 tsconfig.json 则表示 tsconfig.json 所在的目录为你 TypeScript 工程的根目录。

使用 tsconfig.json 的规则:

  • 如果你直接运行 tsc 在这种情况下,编译器将从当前目录开始搜索 tsconfig.json 并向上查找
  • 你也可以直接运行 tsc -p xxx 在这种情况下,xxx 目录上必须存在 tsconfig.json 文件
  • 如果在命令行中直接输出文件 tsc helloworld.ts 编译器将忽略 tsconfig.json 文件

当然你可以直接使用 tsc --init 在当前运行的目录中创建一个 tsconfig.json(推荐)。

{
  "compilerOptions": {
    /* Basic Options */
    "target": "es5",                          /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */
    "module": "commonjs",                     /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
    // "lib": [],                             /* Specify library files to be included in the compilation. */
    // "allowJs": true,                       /* Allow javascript files to be compiled. */
    // "checkJs": true,                       /* Report errors in .js files. */
    // "jsx": "preserve",                     /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
    // "declaration": true,                   /* Generates corresponding '.d.ts' file. */
    // "declarationMap": true,                /* Generates a sourcemap for each corresponding '.d.ts' file. */
    // "sourceMap": true,                     /* Generates corresponding '.map' file. */
    // "outFile": "./",                       /* Concatenate and emit output to single file. */
    // "outDir": "./",                        /* Redirect output structure to the directory. */
    // "rootDir": "./",                       /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
    // "composite": true,                     /* Enable project compilation */
    // "removeComments": true,                /* Do not emit comments to output. */
    // "noEmit": true,                        /* Do not emit outputs. */
    // "importHelpers": true,                 /* Import emit helpers from 'tslib'. */
    // "downlevelIteration": true,            /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
    // "isolatedModules": true,               /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */

    /* Strict Type-Checking Options */
    "strict": true,                           /* Enable all strict type-checking options. */
    // "noImplicitAny": true,                 /* Raise error on expressions and declarations with an implied 'any' type. */
    // "strictNullChecks": true,              /* Enable strict null checks. */
    // "strictFunctionTypes": true,           /* Enable strict checking of function types. */
    // "strictBindCallApply": true,           /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
    // "strictPropertyInitialization": true,  /* Enable strict checking of property initialization in classes. */
    // "noImplicitThis": true,                /* Raise error on 'this' expressions with an implied 'any' type. */
    // "alwaysStrict": true,                  /* Parse in strict mode and emit "use strict" for each source file. */

    /* Additional Checks */
    // "noUnusedLocals": true,                /* Report errors on unused locals. */
    // "noUnusedParameters": true,            /* Report errors on unused parameters. */
    // "noImplicitReturns": true,             /* Report error when not all code paths in function return a value. */
    // "noFallthroughCasesInSwitch": true,    /* Report errors for fallthrough cases in switch statement. */

    /* Module Resolution Options */
    // "moduleResolution": "node",            /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
    // "baseUrl": "./",                       /* Base directory to resolve non-absolute module names. */
    // "paths": {},                           /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
    // "rootDirs": [],                        /* List of root folders whose combined content represents the structure of the project at runtime. */
    // "typeRoots": [],                       /* List of folders to include type definitions from. */
    // "types": [],                           /* Type declaration files to be included in compilation. */
    // "allowSyntheticDefaultImports": true,  /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
    "esModuleInterop": true                   /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
    // "preserveSymlinks": true,              /* Do not resolve the real path of symlinks. */

    /* Source Map Options */
    // "sourceRoot": "",                      /* Specify the location where debugger should locate TypeScript files instead of source locations. */
    // "mapRoot": "",                         /* Specify the location where debugger should locate map files instead of generated locations. */
    // "inlineSourceMap": true,               /* Emit a single file with source maps instead of having a separate file. */
    // "inlineSources": true,                 /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */

    /* Experimental Options */
    // "experimentalDecorators": true,        /* Enables experimental support for ES7 decorators. */
    // "emitDecoratorMetadata": true,         /* Enables experimental support for emitting type metadata for decorators. */
  }
}

参考资料:

编译器选项

这些编译器选项也是 tsconfig.json 中的配置项

当你不想通过 tsconfig.json 来配置编译器的话,也可以使用如下的编译器选项通过命令行来运行编译器执行编译任务,这里只介绍几个常用的,其他的配置请参考 Compiler Options 表格。

  • --watch 或 -w 监视输入文件的更改并触发重新编译
  • --version 或 -v 查看编译器的版本
  • --types=["字符串"] 类型描述文件的名称列表
  • --target="" 指定编译的 ECMAScript 的版本,默认版本是 ES3
  • --strict 启用所有严格类型检查
  • --sourceMap 生成对应的 .map 文件
  • --outFile 输出到单个文件
  • --outDir 输出到目录
  • --module 或 -m 指定使用哪种模块标准来输出代码,选项有 "None", "CommonJS", "AMD", "System", "UMD", "ES6", "ES2015" or "ESNext"
  • --lib 要包含在编译中的库文件列表,比如 ES2015.Promise
  • --jsx.tsx 文件中支持 JSX 可指定要编译输出的 JSX 类型

与 Webpack 集成

事实上我们不可能只单独的使用 TypeScript 而是要将它融入到现有的技术栈中和工具结合起来,至少目前为止多数的前端工程都将 Webpack 视为标准配置,因此 TypeScript 编译器和 Webpack 集成在一起,这非常有用。

awesome-typescript-loader 是 TypeScript 提供的在 Webpack 中使用的 loader,因此我们只需要:

$ yarn add typescript awesome-typescript-loader source-map-loader --dev

然后在你的 webpack.config.json 文件中配置即可:

var fs = require('fs')
var path = require('path')
var webpack = require('webpack')
const { CheckerPlugin } = require('awesome-typescript-loader');
var ROOT = path.resolve(__dirname)

module.exports = {
  entry: './src/index.tsx',
  devtool: 'inline-source-map',
  output: {
    path: ROOT + '/dist',
    filename: '[name].bundle.js',
    sourceMapFilename: '[name].bundle.map.js',
    publicPath: '//localhost:8889/dist/',
  },
  devServer: {
    inline: true,
    quiet: true,
    contentBase: "./",
    port: 8889
  },
  module: {
    rules: [
      { test: /\.ts[x]?$/, loader: "awesome-typescript-loader" },
      { enforce: "pre", test: /\.ts[x]$/, loader: "source-map-loader" },
    ]
  },
  resolve: {
    extensions: [".ts", ".tsx"],
    alias: {
      '@': path.resolve(ROOT,'src')
    }
  },
  plugins: [
    new CheckerPlugin(),
  ]
}

结语

这篇文章简单介绍了 JavaScript 的历史和 TypeScript 所获取的收益,有一句古话:了解历史才能真正了解这些故事,时代变化 JavaScript 可以说是目前为止唯一实现了 Write Once Run Anywhere 的脚本语言(当然 C 语言才是),它的热度和趋势长久不衰,但 JavaScript 本身也有其语言的缺陷,也许在未来新的标准会慢慢补齐它,至少现在让我们用 TypeScript 来解决你可能面临的问题吧。

发布于 2019-04-03

文章被以下专栏收录