JavaScript 中的函数

JavaScript 中函数分为 function declarationfunction expressionarrow function 三种。其中 function declaration 只能作为语句/声明(statememt) 使用,而 function expression 和 arrow function 可以作为表达式(expression) 使用。

  1. // function declaration with name `"calcRectArea"`
  2. function calcRectArea(width, height) {
  3. return width * height;
  4. }
  5. // function declaration with name `undefined`
  6. // 这个函数对象的 name 属性是 `"default"`
  7. export default function (width, height) {
  8. return width * height;
  9. }
  10. // function expression with name `undefined`
  11. // 这个函数对象的 name 属性是 `"calcRectArea2"`
  12. const calcRectArea2 = function(width, height) {
  13. return width * height;
  14. }
  15. // function expression with name `"calcRectArea22"`
  16. // 这个函数对象的 name 属性是 `"calcRectArea22"`
  17. const calcRectArea22 = function calcRectArea22(width, height) {
  18. return width * height;
  19. }
  20. // arrow function
  21. // 这个函数对象的 name 属性是 `"calcRectArea3"`
  22. const calcRectArea3 = (width, height) => {
  23. return width * height;
  24. }

函数类型入门

  1. // function declaration with name `"calcRectArea"`
  2. function calcRectArea(width:number, height:number):number {
  3. return width * height;
  4. }
  5. // function declaration with name `undefined`
  6. // 这个函数对象的 name 属性是 `"default"`
  7. export default function (width:number, height:number):number {
  8. return width * height;
  9. }
  10. // function expression
  11. const calcRectArea2 = function(width:number, height:number):number {
  12. return width * height;
  13. }
  14. // arrow function
  15. const calcRectArea3 = (width:number, height:number):number => {
  16. return width * height;
  17. }

ts 有很好的 type inference,我们根据参数的类型就可以得知返回值类型了,于是可以忽略返回值类型

  1. // function declaration with name `"calcRectArea"`
  2. function calcRectArea(width:number, height:number) {
  3. return width * height;
  4. }
  5. // function declaration with name `undefined`
  6. // 这个函数对象的 name 属性是 `"default"`
  7. export default function (width:number, height:number) {
  8. return width * height;
  9. }
  10. // function expression
  11. const calcRectArea2 = function(width:number, height:number) {
  12. return width * height;
  13. }
  14. // arrow function
  15. const calcRectArea3 = (width:number, height:number) => {
  16. return width * height;
  17. }

我们可以对被赋值的对象标注类型,于是我们有了:(下面诡异的缩进是 prettier 干的……)

  1. // function expression
  2. const calcRectArea2: (width: number, height: number) => number = function(
  3. width: number,
  4. height: number
  5. ):number {
  6. return width * height;
  7. };
  8. // arrow function
  9. const calcRectArea3: (width: number, height: number) => number = (
  10. width: number,
  11. height: number
  12. ):number => {
  13. return width * height;
  14. };

如果 calcRectArea2 和 calcRectArea3 被标注上类型后,后面的函数的参数和返回值类型就可以被确定了,我们就可以忽略书写他们的类型:

  1. // function expression
  2. const calcRectArea2: (width: number, height: number) => number = function(
  3. width,
  4. height
  5. ) {
  6. return width * height;
  7. };
  8. // arrow function
  9. const calcRectArea3: (width: number, height: number) => number = (
  10. width,
  11. height
  12. ) => {
  13. return width * height;
  14. };

我们可以拿出重复的类型,命名个类型别名

  1. type CalcRectArea = (width: number, height: number) => number;
  2. // function expression
  3. const calcRectArea2: CalcRectArea = function(width, height) {
  4. return width * height;
  5. };
  6. // arrow function
  7. const calcRectArea3: CalcRectArea = (width, height) => {
  8. return width * height;
  9. };

联合类型与交叉类型

联合类型表示一个值可以是几种类型之一。 我们用竖线 | 分隔每个类型,所以 number | string | boolean 表示一个值可以是 number, string,或 boolean。

交叉类型是指某个多个特征的类型共同生成的子类型。与联合类型对应,我们使用 & 来分割。

这里我先不细讲它们了,我会在讲类型兼容性时候细讲一下。

可选参数/默认参数

  1. const calcRectArea = (width: number, height: number = 0) => {
  2. return width * height;
  3. };
  4. const calcRectArea2: (width: number, height?: number) => number = (
  5. width,
  6. height
  7. ) => {
  8. return width * (height === undefined ? 0 : height);
  9. };
  10. const calcRectArea3: (width: number, height?: number) => number = (
  11. width: number,
  12. height: number = 0
  13. ) => {
  14. return width * height;
  15. };

可选参数就是在参数的类型的冒号之前加上一个问号。

加上问号的效果相当于 union 上一个 undefined, 但是他们还有些许不同:

  1. const calcRectArea: (width: number, height?: number) => number = (
  2. width,
  3. height
  4. ) => {
  5. return width * (height === undefined ? 0 : height);
  6. };
  7. const calcRectArea2: (
  8. width: number,
  9. height: number | undefined
  10. ) => number = calcRectArea; // OK
  11. const calcRectArea3: (width: number, height?: number) => number = calcRectArea2; // OK
  12. calcRectArea(1) // OK
  13. calcRectArea2(1) // Error: Expected 2 arguments, but got 1.

在类型兼容的角度他们是一样的,但是在调用者来说,他们允许的参数个数不同。

由于 calcRectArea 中 0 默认推出的类型就是 number,所以可以忽略 height 的类型,写作:

  1. const calcRectArea = (width: number, height = 0) => {
  2. return width * height;
  3. };

剩余参数

  1. function buildName(firstName: string, ...restOfName: string[]) {
  2. return firstName + " " + restOfName.join(" ");
  3. }
  4. const buildNameFun: (fname: string, ...rest: string[]) => string = buildName;

特设多态

  1. function plusNumber(x: number, y: number) {
  2. return x + y;
  3. }
  4. function plusNumberArray(xs: number[], ys: number[]) {
  5. return [...xs, ...ys];
  6. }

如果我们需要实现一个对数组和数字共同的加,我们会写成

  1. function plus(x: number, y: number): number;
  2. function plus(xs: number[], ys: number[]): number[];
  3. function plus(xs: number[], y: number): number[];
  4. function plus(x: number | number[], y: number | number[]):any {
  5. if (typeof x === 'number' && typeof y === 'number') {
  6. return x + y;
  7. } else if (Array.isArray(x) && Array.isArray(y)) {
  8. return [...x, ...y];
  9. } else if (Array.isArray(x) && typeof y === 'number') {
  10. return x.map(i => i + y);
  11. } else {
  12. throw new Error('invalid arguments');
  13. }
  14. }

其中前面三行为函数的类型签名,声明这是一个重载的函数,也就是说,这是一个对于不同类型参数有不同的行为的函数。

第四行不是函数的类型签名,对外部调用者来说是无效的。

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,重载会由编译器根据你的类型来选择特定的实现,叫做特设多态的早绑定。

如果采用交叉类型,则上面的重载函数可以写成

  1. type Plus =
  2. & ((xs: number[], y: number) => number[])
  3. & ((xs: number[], ys: number[]) => number[])
  4. & ((x: number, y: number) => number);
  5. const plus: Plus = (x: number | number[], y: number | number[]): any => {
  6. if (typeof x === 'number' && typeof y === 'number') {
  7. return x + y;
  8. } else if (Array.isArray(x) && Array.isArray(y)) {
  9. return [...x, ...y];
  10. } else if (Array.isArray(x) && typeof y === 'number') {
  11. return x.map(i => i + y);
  12. } else {
  13. throw new Error('invalid arguments');
  14. }
  15. };

这里采用交叉类型而不是联合类型的原因是,Plus 是有三个函数类型的共同特征,任何出现需要这三种函数的地方,我们都可以用 Plus 类型的函数,比如 plus 去传入,所以 Plus 是这三个函数类型的子类型,所以使用交叉类型。