TypeScript 类和函数
先看两篇文章关于 ES5 原型链的,写的特别好,图画的就更好了,参考资料:
帮你彻底搞懂JS中的prototype、proto与constructor(图解)
详解JavaScript中的new操作符
忍不住盗图一张:
原型链:f1.__proto__.__proto__.__proto__ === null;
一、TypeScript 类
1.1 介绍
TypeScript 的 class 和 ES2015 的 class 并不是完全一样的概念,ES6 的 class 仅仅是一种语法糖,但是这种语法糖已经成为标准,并且(新)浏览器内核基本支持,而平时使用的时候,基本都是通过 webpack 或者 gulp 这种进行兼容或者打包。
而 TypeScript 的 class 可以使得开发者在不依赖 ES5 的环境下直接使用,不需要等到下个 JS 版本。类的形式在 JavaScript 中有两种写法,ES5 的类和 ES6 的类,决定 TypeScript 翻译成哪种在 tsconfig.json 里面配置:
"target": "ESNEXT", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
1.2 TypeScript 类与 ES6 类的不同。
ES6 的类有 extends 和 static 这点和 TypeScript 没啥不同,不同的是 ES6 的属性修饰符只有默认公开属性和一个私有属性(还是在提案中)。私有属性还和 TypeScript 不同,使用 # 来定义的,官方还给出了理由。
之所以要引入一个新的前缀#表示私有属性,而没有采用private关键字,是因为 JavaScript 是一门动态语言,没有类型声明,使用独立的符号似乎是唯一的比较方便可靠的方法,能够准确地区分一种属性是否为私有属性。另外,Ruby 语言使用@表示私有属性,ES6 没有用这个符号而使用#,是因为@已经被留给了 Decorator
ES 和 TS 比除了属性修饰符不同,没有抽象类之外,其他的都差不多。
1.3 属性修饰符
1、public 默认
一些语言的默认属性修饰符是 protected,而 TypeScript 中成员属性默认都是 public,除了默认之外,也可以强制手动指定。
特点是:可以被继承、外部实例和内部 this 可以访问
2、private 私有属性
特点是:只能内部访问
3、protected
构造函数如果使用 protected 声明,说明这个类不能被实例化,只能被继承。
特点是:外部实例不能访问
4、readonly 修饰符
readonly 修饰符用于将属性设置为只读,如果要设置值,则只能在声明或者是在构造函数中初始化内容。
5、参数属性
定义的属性可以赋值默认值,不在构造函数进行初始化。
1.4 抽象类
抽象类作为其他子类的基类,一般不进行直接实例化,特别的像接口,但和接口不同的是,抽象类可以去实现成员属性或者是方法,然后子类再去覆写成员属性或者方法。
abstract class Person {
abstract talk(): void;
walk(): void {
console.log('...')
}
}
和其他高级语言的面向对象抽象类一样,如果一个方法被定义为 abstract(抽象方法),则这个方法可以不在基类中实现,但是它的子类必须去实现这个方法。抽象方法必须在抽象类里面。
abstract class Person {
abstract talk(): void;
walk(): void {
console.log('...')
}
}
new Person();//不允许,直接报错
class Male extends Person{
talk(){}//必须有这个
}
1.5 TypeScript 类的其他知识
去参考:http://es6.ruanyifeng.com/#docs/class
二、函数
TypeScript 的函数主要增加了强类型判断和一个函数重载知识点。
2.1 函数类型
下面两个函数声明了参数的类型以及函数返回值的类型,上面是具名函数,下面是匿名函数
function add(x: number , y: number ): number {
return x + y;
}
let myAdd = function(x: number, y: number) :number {
return x + y;
}
虽然上面已经定义了函数的类型,但是匿名函数并没有定义完整的函数类型,完整的函数类型:
let myAdd: (x: number, y: number) => number = function (x: number, y: number) : number {
return x + y;
}
完整的函数类型包括:参数类型 和 返回值类型
首先声明了参数类型 :(x: number, y: number)
,然后声明了返回值类型 => number =
。
而参数的名字实际上可以不一致,上面示例中虽然写了相同的名字,但是可以写成两个不同的参数名称:
let myAdd: (x1: number, y1: number) => number = function (x: number, y: number) : number {
return x + y;
}
返回值类型通过 =>
进行表示,如果一个函数返回值是 void ,也必须声明是 void,不能省略因为完整的函数类型包含参数类型和返回值类型两个部分,一个也不能省略。
let myAdd: (x1: number, y1: number) => void = function (x: number, y: number) : number {
return x + y;
}
函数的类型只是由参数类型和返回值组成的。
函数中使用的捕获变量不会体现在类型里。 实际上,这些变量是函数的隐藏状态并不是组成API的一部分。
所谓的捕获变量,与函数本身是无关的,因为这个是由函数作用域外(父级作用域或更高级作用域)声明的变量,只是在函数中使用了而已。
const a:number = 1;
let add:(x: number) => number = function(x: number) :number {
return a + x;
}
推断类型
完整的函数类型中,需要我们明确标示参数的类型是什么,但是如果两边的参数中只有一边的参数声明了类型,typescript 会尝试去识别类型:
let myAdd: (baseValue: number, increment: number) => number =
function(x, y) { return x + y; };
上面代码中,因为 baseValue
和 increment
已经声明了类型是 number,所以 x 和 y 会推断出类型是 number。
2.2 可选参数和默认参数
TypeScript 里的每个函数参数都是必须的,这不是指不能传递 null
或 undefined
作为参数,而是说编译器检查用户是否为每个参数都传入了值。
编译器还会假设只有这些参数会被传递进函数。
简短地说,传递给一个函数的参数个数必须与函数期望的参数个数一致。
function add(x: number, y: number) :number {
return x + y;
}
add(1,2);
add(1); // 错误
add(1,2,3); // 错误
上面 add 方法的使用中,后面两种都是错误的,因为传入的参数个数与期望的个数不相符合
如果需要声明某个参数是可选参数,则可以通过 ?
来标示:
function add(x:number, y:number, z?: number): number {
z = z || 0;
return x + y + z;
}
上面代码中通过 z?
声明 z 可选参数,因此传入 2 个或者3个参数都是可以的。
然而上面代码为了兼容 z 没有值得时候,写了个默认值处理 z = z || 0
,而 ES6 有了函数参数默认值特性之后,这种写法基本都是被废弃的,在 TypeScript 中,可以使用同样的方式声明函数参数的默认值:
function add(x:number, y:number, z:number = 0): number {
return x + y + z;
}
add(1, 3);
add(1,2,3);
add(1,2,3,4)
而一旦声明了默认值之后,就表示这个值可有可无,类似于已经加了 z?
声明。
需要注意的是,可选参数必须接在必需参数后面。换句话说,可选参数后面不允许再出现必需参数了
2.3 剩余参数
ES6 同样拥有 rest 参数,也就是剩余参数。它可以表示任意剩下的参数,本质是一个数组。
typescript 中同样通过省略号来表示剩下的多个参数:
function add(x: number, ...otherNum:number[]): number {
otherNum && otherNum.forEach((item) => {
x += item;
});
return x;
}
add(1, 3, 4, 54, 5, 6);
2.4 重载
重载:允许一个函数接受不同数量或类型的参数时,作出不同的处理。比如,我们需要实现一个函数 reverse,输入数字 123 的时候,输出反转的数字 321,输入字符串 'hello' 的时候,输出反转的字符串 'olleh'。
利用联合类型,我们可以这么实现:
function reverse(x: number | string): number | string {
if (typeof x === 'number') {
return Number(x.toString().split('').reverse().join(''));
} else if (typeof x === 'string') {
return x.split('').reverse().join('');
}
}
然而这样有一个缺点,就是不能够精确的表达,输入为数字的时候,输出也应该为数字,输入为字符串的时候,输出也应该为字符串。
这时,我们可以使用重载定义多个 reverse 的函数类型:
function reverse(x: number): number;
function reverse(x: string): string;
function reverse(x: number | string): number | string {
if (typeof x === 'number') {
return Number(x.toString().split('').reverse().join(''));
} else if (typeof x === 'string') {
return x.split('').reverse().join('');
}
}
上例中,我们重复定义了多次函数 reverse,前几次都是函数定义,最后一次是函数实现。在编辑器的代码提示中,可以正确的看到前两个提示。
但是需要注意的是重载函数只是两个函数声明,并没有函数体。所以上面虽然有三个函数但是只有两个重载函数。
TypeScript 会优先从最前面的函数定义开始匹配,所以多个函数定义如果有包含关系,需要优先把精确的定义写在前面。