什么是TypeScript
TypeScript是一门基于JavaScript之上的编程语言。他重点解决了JavaScript语言自有的类型系统的不足。
通过使用TypeScript这样一个语言可以大大提高代码的可靠程度,虽然说这里的标题只是TypeScript, 但是我们这里其实要去介绍的内容远不止这些,因为我们这里其实要重点去探讨的是JavaScript*类型系统的问题,以及如何去借助一些优秀的技术方案去解决这些问题。
而TypeScript只是在这个过程当中我们会涉及到的一门语言,那因为TypeScript这门语言目前可以说是此类问题的最终极解决方案,所以说我们也会着重去学习他。
除此之外我们也会去介绍一些其他的相关技术方案。我们这里将本次的内容大致分为一下几个阶段。
首先我们去了解一下到底什么是强类型什么是弱类型。什么是静态类型什么是动态类型,那他们之间到底有什么不一样。以及我们为什么JavaScript是弱类型的,还有为什么是动态类型的。
然后我们再去一起了解一下JavaScript自有类型系统存在的问题,以及这些问题给我们的开发工作都造成了哪些影响。
那再往后我们需要去了解一下Flow和TypeScript这两个最主流的JavaScript的类型系统方案。
其中Flow只是一个小工具,他弥补了JavaScript类型系统的不足,而TypeScript则是基于JavaScript基础之上的一门编程语言。所以说相对而言需要了解的内容会更多。
不过也不需要担心,TypeScript他也属于渐进式的,即便说你什么特性都不知道,你也可以立即按照JavaScript的语法去使用他,所以说我们再学习上来讲的话就可以学一点用一点。
强类型与弱类型
在具体介绍JavaScript的类型系统问题之前,我们先来解释两组在区分不同编程语言时,经常提及的名词,分别是强类型和弱类型还有就是静态类型与动态类型。那他们非别是从类型安全和类型检查这两个维度去区分了不同的编程语言。
那首先我们先来看类型安全的这样一个维度,那从类型安全的角度来说编程语言分为强类型和弱类型,那这种强弱类型的概念呢最早是1974年的时候美国两个计算机专家提出的。
那当时对强类型这样一个概念的定义就是在语言层面就限制了函数的实参类型必须要跟形参类型完全相同。
例如我们有一个叫做foo的函数, 那他需要接收一个int类型参数,那我们在调用的时候就不允许直接去传入一个其他类型的值。我们可以选择在传入之前先将我们这个值转换成一个整形的数字,然后再去传入。
class Main {
static void foo(int num) {
System.out.println(num);
}
public static void main(String[] args) {
Main.foo(100); // ok
Main.foo('100'); // error "100" is a string
Main.foo(Integer.parserInt("100")); // ok
}
}
而弱类型呢则完全相反,他在语言层面并不会去限制我们实参的类型,即便我们函数需要的参数是整型的一个数字,我们在调用时仍然可以传入任意类型的数据,语法上是不会报错的。那在运行上有可能会出现问题,但语法上不会有问题。
那由于这种强弱之分呢根本不是某一个权威机构的定义,而且当时这两位计算机的专家他也没有给出一个具体的规则。所以就导致了后人对这种界定方式的细节出现了一些不一样的理解。
但是整体上大家的界定方式都是在描述强类型是有更强的类型约束,而弱类型语言几乎没有什么类型上的约束。
那我个人比较同意的一个说法就是,强类型语言当中不允许有任意的隐式类型转换,而在弱类型语言当中则允许任意的隐式数据类型转换。例如我们这里需要的明明是一个数字,你这放一个字符串,也是可以的,因为他会做隐式类型转换。
我们这里可以来做一些尝试,我们以JavaScript为例,那在JavaScript当中他就允许任意的隐式类型转换,比如我们在代码当中可以直接去尝试使用数学运算符去计算一个字符串和一个数字之间的差。
'100' - 50; // 50
那这种用法呢他并不会报错,那这里的'100'他会自动地被隐式转换为一个数字100,然后进行运算。
那再比如我们调用Math.floor方法,那按照道理来说,这个方法他应该接收一个数字,但是我们实际上传入的一个参数可以是任意的类型,我们在调用的时候都不会报错。
Math.floor('foo'); // NaN
Math.floor(true); // 1
当然有人可能会说,我们在JavaScript当中去调用某些方法时也会报出类型错误,例如我们使用NodeJavaScript环境,在这个环境我们可以使用path模块提供的dirname方法去获取一个路径中的文件夹路径。
path.dirname(111); // TypeError
如果我们传入的不是一个字符串,这里就会报出一个类型错误,难道这就意味着我们JavaScript是强类型了吗?当然不是。
我们这里所说的强类型是从语言的语法层面就限制了不允许传入不同类型的值,那如果我们传入的是不同类型的值,我们在编译阶段就会报出错误,而不是等到运行阶段在通过逻辑判断去限制。
在JavaScript当中所有报出的类型错误都是在运行时通过逻辑判断手动抛出的,例如上面抛出的TypeError,我们就可以在NodeJavaScript的源码当中看到,他确实是通过逻辑判断在vaildateSring(path, 'path')这个方法里面却抛出的一个异常。而不是我们语言或者说语法层面对应的类型限制。
这里我们可以再来看一个强类型的例子,比如说Python。我们使用字符串的100减去数字的50
'100' - 50;
结果就报出了一个不允许在字符串和整数之间使用-这个运算符,那也就是一个类型的错误。
然后我们再来尝试使用py当中的一个全局函数,abs也就是绝对值函数,这个函数要求传入的是一个数字,我们尝试传入一个字符串
abs('foo');
结果同样是报错的,那需要注意的是这里的错误他是从语言层面就报了对应的错误。
那这里我们再来总结一下强类型和弱类型这两种类型之间的差异,强类型他就是不允许有随意的隐式类型转换,而弱类型他是比较随意的,他可以有任意的隐式类型转换,当然这这是我理解的一种强弱类型的界定方式,并不是一个权威的说法。业界也根本没有一个权威的说法。你可以根据自己的理解去做一个定义。
至于你可能会想到我们在代码当中我们的变量类型可以随时改变这样一个特点,其实这并不是强弱类型之间的区别,我们就拿py来说,他是一门强类型的语言,但是他的变量仍然是可以随时改变类型的,那这一点在很多资料当中可能都表述得有些不太妥当,他们都在说py是一门弱类型语言,其实不是这样的。
静态类型与动态类型
那除了类型安全的角度有强类型和弱类型语言之分,在类型检查的角度我们还可以将编程语言分为静态类型语言和动态类型语言。
那关于静态类型语言和动态类型语言之间的差异呢并没有什么争议,大家都很统一。
对于静态类型的语言最主要的表现就是一个变量声明时他的类型就是明确的,而且在这个变量声明过后,它的类型就不允许再被修改。
相反,动态类型语言的特点就是在运行阶段才能明确一个变量的类型,而且变量的类型也可以随时发生变化。例如我们在JavaScript当中我们通过var声明一个foo变量,我们先让他等于100。
那程序运行到这一行的时候才会明确foo他的类型是一个number,然后再将他的值修改为一个字符串,那这种用法也是被允许的。
var foo=100;
foo='bar'; // ok
console.log(foo);
那我们也可以说在动态类型语言中他的变量是没有类型的,而变量当中所存放的值是有类型的。那我们的JavaScript他就是一门标准的动态类型语言。
那总的来说从类型安全的角度来说一般项目的编程语言分为强类型和弱类型。那两者之间的区别就是是否允许随意的隐式类型转换。
那从类型检查的角度一般分为静态类型和动态类型,那他们两者之间的区别就是是否允许随时去修改变量的类型。
需要注意的是这里我们不要混淆了类型检查和类型安全这两个维度,更不要认为弱类型就是动态类型,强类型就是静态类型。这种说法是完全不正确的。
强类型&静态类型: C#, Scala, Java, F#, Haskel
强类型&动态类型: Erlang, Groovy, Python, Clojure, Ruby, Magik
弱类型&静态类型: C, C++
弱类型&动态类型: Perl, PHP, VB, JavaScript
JavaScript类型系统特征
由于JavaScript是一门弱类型而且是动态类型语言,那语言本身的类型系统是非常薄弱的,甚至我们也可以说JavaScript根本就没有一个类型系统,那这种语言的特征用一个比较流行的词来说就是任性。
因为他几乎么没有任何类型的限制,所以说我们JavaScript这么语言也是极其灵活多变的,但是在这种灵活多变的表象背后,丢失掉的就是类型系统的可靠性。
我们在代码中每每遇到一个变量我们都需要去担心他到底是不是我们想要的类型,那整体的感受用另外一个流行的词来说就是不靠谱。
那可以有人会问,为什么JavaScript不能设计成一门强类型或者说静态类型的这种更靠谱的语言呢。
那这个原因自然跟JavaScript的设计背景有关,首先在早前根本就没有人想到JavaScript的应用会发展到今天这种规模。
最高的JavaScript应用根本就不会太复杂,需求都非常简单,很多时候几百行代码甚至是几十行代码就搞定了。
那在这种一眼就能够看到头的这种情况下,类型系统的限制就会显得很多余或者说很麻烦。
那其次JavaScript是一门脚本语言,脚本语言的特点就是不需要编译就直接运行环境当中去运行,那换句话说JavaScript他是没有编译环节的。那即便把他设计成静态类型的语言也没有什么意义。
因为静态类型的语言需要在编译阶段去做类型检查,而JavaScript他根本就没有这样一个环节。
那根据以上这样一些原因,JavaScript就选择成为了一门更灵活更多变的弱类型以及动态类型语言。
那放在当时的那样一个环境当中这并没有什么问题,甚至也可以说这些特点都是JavaScript的一大优势。
而现如今我们前端应用的规模已经完全不同了,遍地都是一些大规模的应用。那我们JavaScript的代码呢也就会变得越来越复杂,开发周期也会变得越来越长。
那在这种情况下,之前JavaScript弱类型动态类型的这些优势也就自然变成了他的短板。
那这个道理其实很好理解,我们打个比方,那以前呢我们只是杀鸡用小刀子就可以了,而且小刀子更灵活更方便,但是现在我们要拿这把小刀去杀牛,那就显得非常吃力了。
那在这里我们的吃力具体到底是体现在什么地方呢,这些我们接下来可以从一些具体的情况当中去体现出来。
弱类型的问题
接下来我们具体来看JavaScript这种弱类型的语言在去应对大规模应用开发时,可能会出现的一些常见的问题。当然我们这里所列举的问题只是冰山一角。不过呢,他们也都能充分反应弱类型的问题。
首先我们先来看第一个例子,这里我们先去定义一个叫做obj的对象。然后我们去调用这个obj的foo方法。
const obj={};
obj.foo();
很明显,这个对象中并不存在这样一个方法,但是在语言的语法层面这样写是可行的。只是我们把这个代码放在环境当中去运行,就会报出一个错误。
那也就是说在JavaScript这种弱类型的语言当中,我们就必须要等到运行阶段才能够去发现代码当中的一些类型异常。
而且这里如果不是立即去执行foo方法而是在某一个特定的时间才去执行,例如我们把它放在timeout的回调当中。
const obj={};
setTimeout(()=> {
obj.foo();
})
那程序在刚刚启动运行时,还没有办法去发现这个异常,一直等到这行代码执行了,才有可能去抛出这样一个异常。
那这也就是说,如果我们是在测试的过程中没有测试到这行代码,那这样一个隐患就会被留到我们代码当中。
而如果是强类型的语言的话,那在这里我们直接去调用对象中一个不存在的方法,这里语法上就会报出错误。根本不用等到我们去运行这行代码。
那我们再来看看第二个例子,这里我们定义一个sum函数,那这个函数他接收两个参数,然后在内部返回这两个参数的和。
那这样一个函数的作用呢,顾名思义,就是去计算这两个数的和,那如果我们调用的时候传入的是两个数字的话,结果自然是正常的。但是如果调用的时候传入的是字符串那这种情况下我们这个函数的作用就完全发生了变化。
function sum (a, b) {
return a + b;
}
console.log(sum(100, 100)); // 200
console.log(sum(100, '100')); // 100100
那这就是因为类型不确定所造成的一个最典型的问题,那可能有人会说我们可以通过自己的约定去规避这样的问题,的确通过约定的方式是可以规避这种问题,但是你要知道约定是根本没有任何保障的。特别是在多人协同开发的时候,我们根本没有办法保证每个人都能遵循所有的约定。
而如果我们使用的是一门强类型的语言的话,那这种情况就会被彻底避免掉,因为在强类型语言中,如果我们要求传入的是数字,那你传入的是其他类型的值,在语法上就行不通。
那我们再来看第三个例子,这里我们先去创建一个对象,然后我们通过索引器的语法去给这个对象添加属性,那我们前面也介绍过,对象的属性名只能够是字符串,或者是ES所推出的Symbal。
但是由于JavaScript是弱类型的,所以说我们这里可以在索引器当中使用任意类型的值去作为属性,而在他的内部会自动转换成字符串。
例如我们这里为这个obj去添加一个true的一个布尔值作为属性名,那最终这个对象他实际的属性名就是字符串的true,也就说我们使用'true'也可以取到这样一个值。
const obj={};
obj[true]=100;
console.log(obj['true']); // 100
那这有什么问题呢?如果说我们不知道对应属性名会自动转换成字符串的这样一个特点,那这里你就会感觉很奇怪,那这种奇怪的根源就是我们用的是一个比较随意的弱类型语言。
那如果是强类型语言的话,那这种问题可以彻底避免,因为在强类型的情况下这里索引器他明确有类型要求,我们不满足类型要求的这样一个成员在语法上就行不通。
综上,弱类型这种语言他的弊端是十分明显的,只是在代码量小的情况下这些问题我们都可以通过约定方式去规避。
而对于一些开发周期特别长的大规模项目,那这种约定的方式仍然会存在隐患,只有在语法层面的强制要求才能够提供更可靠的保障。
所以说强类型语言的代码在代码可靠程度上是有明显优势的,那使用强类型语言呢就可以提前消灭一大部分有可能会存在的类型异常,而不必等到我们在运行过程中再去慢慢的debug。
强类型的优势
通过刚刚对JavaScript这种弱类型语言弊端的一个分析,我们强类型的优势呢已经体现出来了,不过关于强类型的优势还远不止这些。
那这里我们可以提前去总结一下,我们这里可以先总结四个大点。
首先第一点就是错误可以更早地暴露,也就是我们可以在编码阶段提前去消灭一大部分有可能会存在的类型异常。
因为在编码阶段语言本身就会把这些异常把他暴露出来,所以说我们就不用等到运行阶段,再去查找这种错误,那这一点在刚刚的几个例子当中就已经充分体现出来了,我们这就不用再单独表现了。后面的案例还会不断地体现这一点。
那第二点就是强类型的代码会更加智能,然后我们的编码也会更加准确一点,那这是一个开发者更容易感受到的点。
试想一下你为什么需要开发工具的智能提示这样的功能,虽然我以前一直说不要去用智能提示,这只是针对于学习阶段而已,因为在学习阶段如果过度依赖智能提示,这样会对我们编码能力的提升没有任何的帮助。
但是我们在实际开发时肯定是怎么提高效率怎么来,智能提示它能够有效地提高我们编码的效率以及编码的准确性。
但是我们在实际去编写JavaScript的过程当中你会发现,很多时候我们的智能提示不起作用,这是因为开发工具很多时候没有办法推断出来当前对象是个什么类型的,所以也就没有办法知道它里面有哪些具体的成员了。
那我们这时候就只能凭着记忆中的成员名称去访问这些对象当中的成员。那很多时候我们都会因为单词拼错了,或者是成员名称记错了,就会造成一些问题。
如果是强类型语言的话,编辑器是时时刻刻都知道每一个变量到底是什么类型,所以说他就自然能够提供出来更准确的智能提示,那我们的编码也就会更加准确,更加有效率。
那第三点就是使用强类型语言我们的重构会更加牢靠一点,那重构一般是指对我们代码有一些破坏性的改动,例如我们去删除对象中的某个成员,或者是修改一个已经存在的成员名称。
例如我们这里先去定义了一个util对象,在这个对象里我们定义了一个工具函数,那假设这个对象在我们项目当中有很多地方都用到了。
那我们五个月过后你突然发现你之前定义的这个属性名有点草率,你想要把他改成一个更有意义的名称。这个时候我们是不敢轻易修改的。
因为JavaScript是一个弱类型的语言,修改了这样一个成员名称过后,在很多地方用到的这个名称还是以前的名称,即便说有错误,也没有办法立即表现出来。
const util={
aaa: ()=> {
console.log('util func');
}
}
如果是强类型的语言的话,一旦对象的属性名发生了变化,我们在重新编译时就会立即报出错误,那这个时候就可以轻松定位所有使用到这个成员的地方,然后修改他们。
甚至是有些工具还可以自动的把所有引用到这个对象当中的成员的地方自动的修改过来。所以说非常方便,那这也是强类型语言为我们的重构提供了一种更牢靠更可靠的一种保障。
那第四点就是强类型的语言他会减少我们在代码层面不必要的一些类型判断,我们还是以sum函数为例。
function sum (a, b) {
return a + b;
}
因为JavaScript是一个弱类型的语言,所以这里实际接收到的参数有可能是任意的类型,我们为了保证参数的类型我们就必须要通过代码去做一些类型的判断。我们可以使用typeof去分别判断a和b是否都是数字。
function sum (a, b) {
if (typeof a !=='number' || typeof b !=='number') {
throw new TypeError('arguments must be a number')
}
return a + b;
}
那这里我们所编写的类型判断代码他实际的目的就是为了保证我们拿到的数据类型是我们这里所需要的number。
而如果是强类型语言的话,那这段判断根本是没有任何的意义的,因为不是我们所需要的类型根本就传不进来,只有弱类型语言才会需要这种特殊的类型判断。
那以上就是强类型语言的一些典型的优势,当然了这里我们所有对强类型语言的那些如果都是建立在你有接触过一些强类型语言的基础之上的。
那如果说你之前没有接触过任何强类型语言,可能会没有太多的概念,那没有关系,我们也可以带着这些所谓的期待去接着往下做一些深入的探索。