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 expression
const calcRectArea2 = function(width:number, height:number):number {
return width * height;
}
// arrow function
const 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 expression
const calcRectArea2 = function(width:number, height:number) {
return width * height;
}
// arrow function
const calcRectArea3 = (width:number, height:number) => {
return width * height;
}
我们可以对被赋值的对象标注类型,于是我们有了:(下面诡异的缩进是 prettier 干的……)
// function expression
const calcRectArea2: (width: number, height: number) => number = function(
width: number,
height: number
):number {
return width * height;
};
// arrow function
const calcRectArea3: (width: number, height: number) => number = (
width: number,
height: number
):number => {
return width * height;
};
如果 calcRectArea2 和 calcRectArea3 被标注上类型后,后面的函数的参数和返回值类型就可以被确定了,我们就可以忽略书写他们的类型:
// function expression
const calcRectArea2: (width: number, height: number) => number = function(
width,
height
) {
return width * height;
};
// arrow function
const calcRectArea3: (width: number, height: number) => number = (
width,
height
) => {
return width * height;
};
我们可以拿出重复的类型,命名个类型别名
type CalcRectArea = (width: number, height: number) => number;
// function expression
const calcRectArea2: CalcRectArea = function(width, height) {
return width * height;
};
// arrow function
const 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; // OK
const calcRectArea3: (width: number, height?: number) => number = calcRectArea2; // OK
calcRectArea(1) // OK
calcRectArea2(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 是这三个函数类型的子类型,所以使用交叉类型。