函数
函数式编程
函数式编程的五个特点:
- 函数是第一公民
- 只用表达式,不用语句
- 没有副作用
- 不修改状态
- 引用透明性
其中,函数是第一公民这是语言本身特性决定的;只用表达式不用语句是很难纯粹办到的;没有副作用和不修改状态都有一个非常关键的特点就是不改变参数和全局变量的值。
Erlang
是被推为函数式编程的一个典型代表,事实上JavaScript也可以人为做到这一点。
函数/方法
首先,纠正一下函数
和方法
两个概念,这两个词经常被混用,事实上它们是完全不同的两个东西,函数
是指一个独立可执行的程序过程,而方法
是附着于某个类对象的的程序过程
箭头函数 Arrow Function
箭头函数主要用来解决this问题,为了避免this问题,推荐尽可能地使用箭头函数
为了语义明确,不推荐省略参数处的括号,不省略return
// 不推荐的写法
[1, 2, 3].map(it => ++it);
// 推荐的写法
[1, 2, 3].map((it) => {
return ++it;
});
函数 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
自执行函数(立即执行函数)
((a, b) => {
return a + b;
})(1, 2);
闭包
注意下例中变量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
函数名(命名规范)
函数名是一个函数被调用者调用时使用的一个代号,函数名的命名规范在不同的开发语言,甚至不同的开发团队,都会使用不同的命令规范。我们的函数命名使用了一些方法来使函数命名更容易被接受。这里我们把所有开发用到的一些命名的规范都放在这里。
文件(目录)名
文件(目录)名一部分可以使用编号,如组件编号。一部分使用英文名称,实在不会,找翻译吧。不要使用中文名称,不要使用大写字母,不要使用中文输入法中的全角英文字母,如果有多个词,请在多个词中间使用减号-
分隔开。
变量(常量)、函数、参数名
不要使用拼音,尤其是简拼。不要使用大写字母。如果有多个词,请使用下划线_
分隔开。
接口名,类型重定义名称
使用首字母大写的驼峰式写法
枚举名
使用首字母小写的驼峰式写法
参数
形参
形参,即形式参数,即函数在定义的时候在函数体内使用的参数,目的是用来接收调用该函数时传递的实际参数(实参)
实参
如上所述,在调用函数时传递给该函数的实际的参数,即为实参。
实参可以是一个变量,可以是一个常量,还可以是一个表达式(其实这种依然可以将其理解为一个常量,即表达式的值),甚至,在JavaScript里,它可以是一个函数(还很常用),这也是函数为第一公民的一个特性。
可选参数
function fun(name: string, age?: number) {
if (age) {
return {
age,
name
};
} else {
return {
age: 0,
name
};
}
}
缺省参数
function fun(name: string, age = 0) {
return {
age,
name
};
}
不定参数
相信不少人对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
箭头函数(Arrow functions/lambda表达式)
在上面例子中,我们多次使用到了箭头函数,箭头函数是es6的一个新特性,在TypeScript中,我们可以使用所有最新的新语法的新特性(只是关于函数的,就我所了解,有一些类的特性是不能编译为es3,es5的),而基本不不用关心它的兼容性(虽然兼容性问题实际上我们通常是使用babel来解决的)。
尽管箭头函数努力地使它与lambda表达式接近,但我觉得弊大于利,还是老老实实把它当函数用吧。它带给我们的好处就是,this
不再可变,它从被调用的地方继承。但实际上我们不使用this
,使用箭头函数是一个好习惯,它使你从此不再担心this
的问题,岂非一件非常美好的事?
什么时候用箭头函数呢?答案就是当使用回调函数的时候。
回调函数
因为JavaScript是事件驱动的,所以回调函数是必不可少的,但多次回调的跟一次回调的还是有区别的,如以下例子:
setTimeout(() => {
// todo
}, 3000);
setInterval(() => {
// todo
}, 3000);
第一个例子的回调函数只会被调用一次,而第二个例子中的回调函数则会每隔3秒就会被执行一次。有趣的是:只有第一种情况的回调才会出现回调地狱
。而第一种正是我们可以通过Promise来使其逃离地狱的应用场景。
异步
回调和异步实际上是不分家的,有回调就有异步。反过来有异步一定是由回调造成的。
如以下例子,需要使体统休眠一定时间后再进行其它操作,通常的实现方式如下:
function sleep(time: number, finish: () => void) {
setTimeout(() => {
finish();
}, time);
}
如果我们要调用它,代码大概如下:
sleep(3000, () => {
// todo
});
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
,用英国话说,这叫一个承诺
,那什么是承诺呢?就是我跟你说,过一万年后,我给你一千个亿人民币。
我
我是主语,做事情的人是我。你
你是宾语,一万年后,是你拿着这一千个亿在花。过一万年后
承诺肯定需要一个时间,只要有承诺就不是马上就兑现,如果马上能兑现就不需要承诺了,即使有时候是0毫秒,记住,也不是马上兑现,因为JavaScript的事件驱动机制,如果是0毫秒,它也是在事件队伍中排除,只不过排地比较靠前罢了。具体参见网上的一篇文章node.js中的定时器nextTick()和setImmediate()区别分析我给你一千个亿人民币
在承诺的时间结束后,我给你的一个结果,当然,有可能像上面例子当中,什么结果也不给(void
,注意给0元和不给是两回事)
这个承诺到达承诺的时间后,我有可能会达成这个承诺的目标(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);
});
}
});
}
Generator
这一部分太拗口,并且真不好理解,只是一个过渡,实际上被使用的时期也不长,甚至不如Promise来得持久,所以这里不再深入讲解,免得给大家带来更多困扰。有兴趣的话,找篇文章看看吧.该文我随意搜来,没仔细看,不保证质量,误人莫怪。
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.');
}
是不是更亲切了?不过还是有一些需要注意的:
- 只能在被声明为异步(
async
)的函数中使用await
关键字。 - await 可以应用于异步或非异步的函数调用前,无论内有多少层Promise,await都会等待,一直到Promise返回的值不是一个Promise。
导入/导出
我们将功能模块分为一个个的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
}