函数

1. 定义

函数的概念与数学中的函数概念相似:y=f(x);它还有一种翻译叫作“功能”。直观的理解是:函数可以作一件特定事情的功能块,它的输入就是该函数的参数,它的输出就是该函数的返回值。

2. 组成

function reverse(param: string): string {
    return param.split('').reverse().join('');
}

2.1. 关键字

function

2.2. 函数名

reverse

2.3. 参数

2.3.1. 参数说明

param: string

这个函数有一个参数,这个参数的名字叫param,它的类型为string,如果有多个参数,则多个参数音使用逗号,隔开。如果有默认参数,需要将默认参数放在最右;如果有可选参数,需要将可选参数放在默认参数前(如果有)和必传参数之前(如果有)。

2.4. 形参

形参,即形式参数,即函数在定义的时候在函数体内使用的参数,目的是用来接收调用该函数时传递的实际参数(实参)

2.5. 实参

如上所述,在调用函数时传递给该函数的实际的参数,即为实参。

实参可以是一个变量,可以是一个常量,还可以是一个表达式(其实这种依然可以将其理解为一个常量,即表达式的值),甚至,在JavaScript里,它可以是一个函数(还很常用),这也是函数为第一公民的一个特性。

2.5.1. 可选参数

function fun(name: string, age?: number) {
    if (age) {
        return {
            age,
            name
        };
    } else {
        return {
            age: 0,
            name
        };
    }
}

2.6. 缺省参数

function fun(name: string, age = 0) {
    return {
        age,
        name
    };
}

注意在实际项目中,我不推荐使用缺省参数和可选参数。因为它会让某些开发人员忘掉函数的其它参数,如果真想要固定参数,请减少参数数量:

function add(a: number, b: number): string {
    return a + b;
}

function add3(a: number){
    return add(a, 3);
}

如上例,如果第二个参数b固定为3,那么可以使用add3add进行一次封装。

2.6.1. 多类型参数

如上例,add3希望可以接收一个字符串类型的参数(可能是用户输入或是数据表字段类型的原因)。

function add3(a: number | string){
    return add(+a, 3);
}

2.6.2. 可变参数

这里说的可变指的是参数的个数可变,如上面的add,假如我们不确定参数个数,可以这样写:

function add(...n: number[]) {
    return n.reduce((pre, cur)=>{
        return pre + cur;
    }, 0);
}

这样调用者可以传入0到多个参数。不建议大量使用。

相信不少人对JavaScript中的arguments比较熟悉,在TypeScript中,我们不使用它,因为它是一个数组,如果我们要使用它,有时候还不能方便地将其当作数组使用。这给开发人员带来了不少困惑,在TypeScript中,我们使用真正的数组来访问不定参数

function append(src: string, ...str: string[]) {
    return str.reduce((p, c) => {
        return p + c;
    }, src);
}

不定参数的调用方法如下:

append('hello', 'feidao');      // hello feidao
append('hello', 'fei', 'dao');      // hello feidao

2.6.3. 通用类型函数(generic type)

在C++中,它叫“模板函数”,在Java中它应该叫作“泛型”函数,指的是一个函数的参数或返回值类型不确定,而是由传入参数类型推断或是直接传入类型来确定。

  1. 推断类型

    function get_map_keys<T>(map: Map<string,T>) {
        return Array.from(map.keys());
    }
    
    const map = new Map<string, string | number>();
    ...
    const keys = get_map_keys(map);
    
  2. 传入类型

    function json2obj<T>(str: string) {
        return JSON.parse(str) as T;
    }
    
    const json = '{"foo":"bar"}';
    const obj = json2obj<{ foo: string;}>(json);
    

2.7. 返回类型

返回类型在实际应用中,大部分时候不需要明确写出,ts编译器会根据语法自动推导出返回类型(如上例中的addadd5)。有一种特殊情况:当函数中存在递归调用时,必须在函数参数括号后声明返回类型。

2.8. 函数体

函数体指函数参数小括号后紧跟的最外层大括号包括的部分。函数的主要功能实现就在这里完成。有些函数的函数体很小,有些函数的函数体可能会非常大(有可能有成百上千行的代码)。

2.9. 匿名函数

就是没有名字的函数

const fun = function (){};

这里,fun不是函数名,它只是个变量名,只不过这个变量的值为一个函数。而这个函数是没有名字的,那么我们就把这个函数叫作匿名函数。

2.10. 自执行函数

(() => {})();

一个匿名函数在它被声明后立即被执行了,它有什么用呢?它有一个比较大的作用就是不污染调用处的变量空间(尤其是全局变量)。因为我们不推荐使用全局变量(参见前面章节中关于变量声明部分),所以这个大作用反而对我们是无用的。但很多开源代码中都有大量这种自执行函数,我们去查看时不致困惑就好了。

3. 箭头函数 Arrow Function

不使用函数关键字,而是用一个箭头来代替,这种函数通常没有名字(匿名函数)。像我们前面例子中使用到的数组的相关操作的第一个参数就是箭头函数。

[1, 2, 3, 4].filter((n) => {
    return n % 2 === 0;
});

其中

(n) => {
    return n % 2 === 0;
}

部分就是一个箭头函数,箭头使用的一个非常大的作用是解决了JavaScript中复杂难以理解的this问题。

4. lambda表达式

lambda表达式在TypeScript/JavaScript中可以认为就是省略的箭头函数,个人认为这种只能称之为语法糖的东西除了可以满足一部分人的优越感之外没有太大意义,不推荐使用,这里也不作详细说明。将这种纯粹语法糖演义到极致的还有Java系的Scala的语法。

5. 函数式

函数式也被叫作面向函数编程、面向过程编程。是一种比较贴近自然的编程方法。是将要完成的任务分为步骤一、步骤二、步骤三。

函数式编程的五个特点:

  1. 函数是第一公民
  2. 只用表达式,不用语句
  3. 没有副作用
  4. 不修改状态
  5. 引用透明性

其中,函数是第一公民这是语言本身特性决定的;只用表达式不用语句是很难纯粹办到的;没有副作用和不修改状态都有一个非常关键的特点就是不改变参数和全局变量的值。

Erlang是被推为函数式编程的一个典型代表,事实上JavaScript也可以人为做到这一点。

6. 面向对象

将要分析的内容抽象为类,为类抽象各种属性、方法,最后在使用时将类实例化为对象,再按步骤调用类方法来完成任务。

7. 面向函数vs页面对象

很难说其中一种好过于另外一种,只能说,在合适的场合使用合适的方法来做才是最好的。纯粹地追求某一种方法会走向极端,并不一定会是最合理的方法。

8. 函数/方法

首先,纠正一下函数方法两个概念,这两个词经常被混用,事实上它们是完全不同的两个东西,函数是指一个独立可执行的程序过程,而方法是附着于某个类对象的的程序过程

9. 函数 Function

如果我们要动态生成函数,除了可以使用eval之外,可以显式创建一个Function

const add = new Function('a', 'b', 'return a + b;');
add(1, 2);  // 3

Function构造函数的参数为不定参数,最后一个参数是函数体内部要执行的字符串,前面的参数均为这个函数的形参;

这个函数没有名称,属于匿名函数,但有一个变量add去承载它,注意add不是一个函数名,它只是一个变量。

它相当于这样:

const add = function (a, b) {
    return a + b;
};
add(1, 2);  // 3

10. 闭包

注意下例中变量r的变化

const add = (() => {
  let r = '';
  return (str: string) => {
    return r += str;
  };
})();
add('hello');  // hello
add(' world');  // hello world

使用闭包可以代替一部分类的作用,上面的例子可以使用类这样实现:

class Method {
    private r: string;
    constructor() {
        this.r = '';
    }
    add(str: string) {
        return this.r += str;
    }
}

const m = new Method();
m.add('hello');  // hello
m.add(' world');  // hello world

11. 函数名(命名规范)

函数名是一个函数被调用者调用时使用的一个代号,函数名的命名规范在不同的开发语言,甚至不同的开发团队,都会使用不同的命令规范。我们的函数命名使用了一些方法来使函数命名更容易被接受。这里我们把所有开发用到的一些命名的规范都放在这里。

11.1. 文件(目录)名

文件(目录)名一部分可以使用编号,如组件编号。一部分使用英文名称,实在不会,找翻译吧。不要使用中文名称,不要使用大写字母,不要使用中文输入法中的全角英文字母,如果有多个词,请在多个词中间使用减号-分隔开。

11.2. 变量(常量)、函数、参数名

不要使用拼音,尤其是简拼。不要使用大写字母。如果有多个词,请使用下划线_分隔开。

11.3. 接口名,类型重定义名称

使用首字母大写的驼峰式写法

11.4. 枚举名

使用首字母小写的驼峰式写法

12. 回调函数

因为JavaScript是事件驱动的,所以回调函数是必不可少的,但多次回调的跟一次回调的还是有区别的,如以下例子:

setTimeout(() => {
    // todo
}, 3000);
setInterval(() => {
    // todo
}, 3000);

第一个例子的回调函数只会被调用一次,而第二个例子中的回调函数则会每隔3秒就会被执行一次。有趣的是:只有第一种情况的回调才会出现回调地狱。而第一种正是我们可以通过Promise来使其逃离地狱的应用场景。

13. 异步

回调和异步实际上是不分家的,有回调就有异步。反过来有异步一定是由回调造成的。

如以下例子,需要使体统休眠一定时间后再进行其它操作,通常的实现方式如下:

function sleep(time: number, finish: () => void) {
    setTimeout(() => {
        finish();
    }, time);
}

如果我们要调用它,代码大概如下:

sleep(3000, () => {
    // todo
});

13.1. Promise

前面已经提到,这一类的回调函数是可以转为一个Promise的,如何做呢?代码送上:

function sleep(time: number) {
    return new Promise<void>((resolve) => {
        setTimeout(() => {
            resolve();
        }, time);
    });
}

这个时候我们如果要调用它,代码如下:

sleep(3000).then(() => {
    // todo
});

这似乎看不到Promise的优势,我们调整一下,我们的需求是,休眠3秒之后输出1,再过2秒输出2,再过1秒输出3,如果用回调的方法写,调用时需要这样:

sleep(3000, () => {
    console.log(1);
    sleep(2000, () => {
        console.log(2);
        sleep(1000, () => {
            console.log(3);
        });
    });
});

有木有感觉很麻烦,眼有点儿花?那么现在再来看用Promise怎么写吧:

sleep(3000)
.then(() => {
    console.log(1);
    return sleep(2000)
    .then(() => {
        console.log(2);
        return sleep(1000)
        .then(() => {
            console.log(3);
        });
    });
})

你一定以为这似乎还没有回调函数使用起来简单呢,那是因为我们还没把它整理好,上面的代码虽然是正确的,但总体来说并没有发挥Promise的优势,下面我们再来看改进的一个版本:

sleep(3000).
    .then(() => {
        console.log(1);
        return sleep(2000);
    }).then(() => {
        console.log(2);
        return sleep(1000);
    }).then(() => {
        console.log(3);
    });

这样看起来是不是好多了呢?不论有多少个异步操作,都可以一直使用then链式写法写下去。

这还不是最复杂的情况,复杂(且是一般的使用场景)的情况是回调函数经常会有两个,甚至有三个

function sleep(time: number, success: () => void, fail: () => void) {
    if (time > 0) {
        setTimeout(() => {
            success();
        }, time);
    } else {
        fail();
    }
}

再回到之前的调用(作者想想怎么写例子头都大,这得多么复杂呀):

sleep(3000, () => {
    console.log(1);
    sleep(2000, () => {
        console.log(2);
        sleep(1000, () => {
            console.log(3);
        }, () => {
            console.error('something is wrong.');
        });
    }, () => {
        console.error('something is wrong.');
    });
}, () => {
    console.error('something is wrong.');
});

估计有人会迫不及待想要知道Promise的写法是怎样调用的了吧

sleep(3000).
    .then(() => {
        console.log(1);
        return sleep(2000);
    }).then(() => {
        console.log(2);
        return sleep(1000);
    }).then(() => {
        console.log(3);
    }, () => {
        console.error('something is wrong.');
    }, () => {
        // do nothing
    });

有没有简单一些了?如果你还是不过瘾,我们来换一下场景,因为某种情形,我们需要等待三个结果,比如大家想像一下这样一个场景:张三开发一个页面需要3天(这只是他自己预估的时间,实际上我们根本无法确切知道他进行这个页面的开发需要几天时间),李四开发一个页面需要2天,王五开发一个页面需要1天,他们同时开工,等他们三个全部结束的时候才能共同进行另外一个页面的开发。回调的写法:

let zhangsan_finished = false;
let lisi_finished = false;
let wangwu_finished = false;

zhangsan_dev(callback: () => void) {
    // todo
}
lisi_dev(callback: () => void) {
    // todo
}
wangwu_dev(callback: () => void) {
    // todo
}
dev_another() {
    // todo
}

zhangsan_dev(() => {
    zhangsan_finished = true;
    if (lisi_finished === true && wangwu_finished === true) {
        dev_another();
    }
});

lisi_dev(() => {
    lisi_finished = true;
    if (zhangsan_finished === true && wangwu_finished === true) {
        dev_another();
    }
});

wangwu_dev(() => {
    wangwu_finished = true;
    if (zhangsan_finished === true && lisi_finished === true) {
        dev_another();
    }
});

使用Promise怎么做呢?哈,黑科技来了:

Promise.all([zhangsan_dev(), lisi_dev(), wangwu_dev()]).then(() => {
    dev_another();
});

是不是对Promise有那么一点点感觉了?

Promise,用英国话说,这叫一个承诺,那什么是承诺呢?就是我跟你说,过一万年后,我给你一千个亿人民币。

这个承诺到达承诺的时间后,我有可能会达成这个承诺的目标(resolve),给你一千亿,如果没有到承诺的时间,我是一定不会给你一千亿的。但也有可能还没有到承诺的时间,比如一百年后我挂了,我就不给你了(reject).

function yiqianyi(){
    return new Promise<number>((resolve, reject) => {
        zhengqian();
        huaqian();
        zhengqian();
        huaqian();
        zhengqian();
        huaqian();
        if(Idie(){
            reject(bugeile);
        }) else {
            sleep(一万年)
            .then(()=>{
                resolve(一千亿);
            }, () => {
                reject(nowhy);
            });
        }
    });
}

虽然使用Promise跟之前的回调函数本质上一样的,但这样做在我们看起来,在前一件事调用完成后要不要做其它事不再由函数前一个函数来决定,而是调用者来决定。这样似乎才更合乎逻辑一些。

13.2. Generator

这一部分太拗口,并且真不好理解,只是一个过渡,实际上被使用的时期也不长,甚至不如Promise来得持久,所以这里不再深入讲解,免得给大家带来更多困扰。有兴趣的话,找篇文章看看吧.该文我随意搜来,没仔细看,不保证质量,误人莫怪。

13.3. Async/Await

这是个好东西,使用它,你可以用同步的写法完成异步的代码调用。接着上面的例子,我们继续换成async/await的写法:

try {
    await sleep(3000);
    console.log(1);
    await sleep(2000);
    console.log(2);
    await sleep(1000);
    console.log(3);
} catch (e) {
    console.error('something is wrong.');
}

是不是更亲切了?不过还是有一些需要注意的:

  1. 只能在被声明为异步(async)的函数中使用await关键字。
  2. await 可以应用于异步或非异步的函数调用前,无论内有多少层Promise,await都会等待,一直到Promise返回的值不是一个Promise。

13.4. 怎样让一个函数变成异步的?

有时候我们为了模拟异步函数(或是统一接口),可以用以下几种方法:

Promise.resolve(xxx);
Promise.reject(xxx);
await xxx;  // 这种方法在一些语法检查时会认为是错误的,如果有这类语法检查,请使用 Promise.resolve(xxx);

13.5. 常见的异步操作

  1. 网络请求
  2. 文件读取
  3. setTimeout,setInterval等时间类函数调用
  4. 数据库操作

14. 导入/导出

我们将功能模块分为一个个的ts文件,那么多个ts文件是如何关联起来的呢?答案就是导入/导出(import export)

我们可以将一些函数(或常量/变量)导出,这样,外部就可以访问到这些函数(或常量/变量)了,比如说我们在一个文件add.ts文件中定义了一个函数

add.ts

export function add(a: number, b: number) {
    return a + b;
}

这里export关键字就是用来将其后面定义的函数(或常量/变量)导出,而如果这个文件中其它的函数,则只有export后面的函数导出,相当于公有函数;非export的函数只能被该文件内部的函数调用,相当于私有函数。

test.ts中调用该函数的代码如下:

import { add } from './add';

test() {
    add(1, 2);  // 3
}

有时候,被导入的名字被占用了, 我们需要给被导入的函数另起个别名:

import { add as add_fun } from './add';

test() {
    const base = 1;
    const add = 2;
    add_fun(base, add); // 3
}

还有的时候,我们导入的时候将整个文件中的所的导出的内容全部导入,这样使用起来会简单一些(但是我强烈不推荐使用这种方法)

import * as atoms from './add';

test() {
    atoms.add(1, 2);    // 3
}

使用这种全部导入的方法应该尽量少用,以致尽量不用,因为这种写法目前我们没有有效的方法在打包的时候提取代码。

还有一种导出方法,即默认导出,这种方法在使用的时候简单一些:

export default function add(a: number, b: number) {
    return a + b;
}

使用的方法如下:

import add from './add';

test() {
    add(1, 2);  // todo
}

默认导出的函数的名字如果要改别名,比非default的导出的函数更简单

import add_fun from './add';

test() {
    const base = 1;
    const add = 2;
    add_fun(base, add); // 3
}