JavaScript 中的函数
JavaScript 中函数分为 function declaration, function expression 和 arrow function 三种。其中 function declaration 只能作为语句/声明(statememt) 使用,而 function expression 和 arrow function 可以作为表达式(expression) 使用。
// function declaration with name `"calcRectArea"`function calcRectArea(width, height) {return width * height;}// function declaration with name `undefined`// 这个函数对象的 name 属性是 `"default"`export default function (width, height) {return width * height;}// function expression with name `undefined`// 这个函数对象的 name 属性是 `"calcRectArea2"`const calcRectArea2 = function(width, height) {return width * height;}// function expression with name `"calcRectArea22"`// 这个函数对象的 name 属性是 `"calcRectArea22"`const calcRectArea22 = function calcRectArea22(width, height) {return width * height;}// arrow function// 这个函数对象的 name 属性是 `"calcRectArea3"`const calcRectArea3 = (width, height) => {return width * height;}
函数类型入门
// function declaration with name `"calcRectArea"`function calcRectArea(width:number, height:number):number {return width * height;}// function declaration with name `undefined`// 这个函数对象的 name 属性是 `"default"`export default function (width:number, height:number):number {return width * height;}// function expressionconst calcRectArea2 = function(width:number, height:number):number {return width * height;}// arrow functionconst calcRectArea3 = (width:number, height:number):number => {return width * height;}
ts 有很好的 type inference,我们根据参数的类型就可以得知返回值类型了,于是可以忽略返回值类型
// function declaration with name `"calcRectArea"`function calcRectArea(width:number, height:number) {return width * height;}// function declaration with name `undefined`// 这个函数对象的 name 属性是 `"default"`export default function (width:number, height:number) {return width * height;}// function expressionconst calcRectArea2 = function(width:number, height:number) {return width * height;}// arrow functionconst calcRectArea3 = (width:number, height:number) => {return width * height;}
我们可以对被赋值的对象标注类型,于是我们有了:(下面诡异的缩进是 prettier 干的……)
// function expressionconst calcRectArea2: (width: number, height: number) => number = function(width: number,height: number):number {return width * height;};// arrow functionconst calcRectArea3: (width: number, height: number) => number = (width: number,height: number):number => {return width * height;};
如果 calcRectArea2 和 calcRectArea3 被标注上类型后,后面的函数的参数和返回值类型就可以被确定了,我们就可以忽略书写他们的类型:
// function expressionconst calcRectArea2: (width: number, height: number) => number = function(width,height) {return width * height;};// arrow functionconst calcRectArea3: (width: number, height: number) => number = (width,height) => {return width * height;};
我们可以拿出重复的类型,命名个类型别名
type CalcRectArea = (width: number, height: number) => number;// function expressionconst calcRectArea2: CalcRectArea = function(width, height) {return width * height;};// arrow functionconst calcRectArea3: CalcRectArea = (width, height) => {return width * height;};
联合类型与交叉类型
联合类型表示一个值可以是几种类型之一。 我们用竖线 | 分隔每个类型,所以 number | string | boolean 表示一个值可以是 number, string,或 boolean。
交叉类型是指某个多个特征的类型共同生成的子类型。与联合类型对应,我们使用 & 来分割。
这里我先不细讲它们了,我会在讲类型兼容性时候细讲一下。
可选参数/默认参数
const calcRectArea = (width: number, height: number = 0) => {return width * height;};const calcRectArea2: (width: number, height?: number) => number = (width,height) => {return width * (height === undefined ? 0 : height);};const calcRectArea3: (width: number, height?: number) => number = (width: number,height: number = 0) => {return width * height;};
可选参数就是在参数的类型的冒号之前加上一个问号。
加上问号的效果相当于 union 上一个 undefined, 但是他们还有些许不同:
const calcRectArea: (width: number, height?: number) => number = (width,height) => {return width * (height === undefined ? 0 : height);};const calcRectArea2: (width: number,height: number | undefined) => number = calcRectArea; // OKconst calcRectArea3: (width: number, height?: number) => number = calcRectArea2; // OKcalcRectArea(1) // OKcalcRectArea2(1) // Error: Expected 2 arguments, but got 1.
在类型兼容的角度他们是一样的,但是在调用者来说,他们允许的参数个数不同。
由于 calcRectArea 中 0 默认推出的类型就是 number,所以可以忽略 height 的类型,写作:
const calcRectArea = (width: number, height = 0) => {return width * height;};
剩余参数
function buildName(firstName: string, ...restOfName: string[]) {return firstName + " " + restOfName.join(" ");}const buildNameFun: (fname: string, ...rest: string[]) => string = buildName;
特设多态
function plusNumber(x: number, y: number) {return x + y;}function plusNumberArray(xs: number[], ys: number[]) {return [...xs, ...ys];}
如果我们需要实现一个对数组和数字共同的加,我们会写成
function plus(x: number, y: number): number;function plus(xs: number[], ys: number[]): number[];function plus(xs: number[], y: number): number[];function plus(x: number | number[], y: number | number[]):any {if (typeof x === 'number' && typeof y === 'number') {return x + y;} else if (Array.isArray(x) && Array.isArray(y)) {return [...x, ...y];} else if (Array.isArray(x) && typeof y === 'number') {return x.map(i => i + y);} else {throw new Error('invalid arguments');}}
其中前面三行为函数的类型签名,声明这是一个重载的函数,也就是说,这是一个对于不同类型参数有不同的行为的函数。
第四行不是函数的类型签名,对外部调用者来说是无效的。
js 是一个没有重载的语言,所以我们在函数实现体内部做了运行时的类型检查。 ts 在类型签名上给了我们补偿,让我们能在使用函数时候得到正确的参数检查和返回值推断。
注意,这里的每一行类型签名都是要手动写的,而且对不同类型的处理也是要程序员手动做的,所以它只支持独立的特定的某些类型。
特设多态,(ad-hoc polymorphism)就是指对函数的重载。
ad-hoc polymorphism,ad prep. Latin “to (+accusative)”, hoc Latin accusative neuter singular of hic “this”,直译to this/for this,意思是针对每个(type)的polymorphism。 —- by Yutong Zhang
顺便提一下,在其他语言中,比如 C++ 或者 Java,重载会由编译器根据你的类型来选择特定的实现,叫做特设多态的早绑定。
如果采用交叉类型,则上面的重载函数可以写成
type Plus =& ((xs: number[], y: number) => number[])& ((xs: number[], ys: number[]) => number[])& ((x: number, y: number) => number);const plus: Plus = (x: number | number[], y: number | number[]): any => {if (typeof x === 'number' && typeof y === 'number') {return x + y;} else if (Array.isArray(x) && Array.isArray(y)) {return [...x, ...y];} else if (Array.isArray(x) && typeof y === 'number') {return x.map(i => i + y);} else {throw new Error('invalid arguments');}};
这里采用交叉类型而不是联合类型的原因是,Plus 是有三个函数类型的共同特征,任何出现需要这三种函数的地方,我们都可以用 Plus 类型的函数,比如 plus 去传入,所以 Plus 是这三个函数类型的子类型,所以使用交叉类型。
