-
要有输入参数。如果没有输入参数,这个函数拿不到任意外部信息,也就不用运行了。
-
要有返回值。如果有输入没有返回值,又没有副作用,那么这个函数白调了。
-
对于确定的输入,有确定的输出
做到这一点,说简单也简单,只要保持功能足够简单就可以做到;说困难也困难,需要改变写惯了命令行代码的思路。
比如数学函数一般就是这样的好例子,比如我们写一个算平方的函数:
let sqr2 = function(x){
return x * x;
}
console.log(sqr2(200));
无副作用函数拥有三个巨大的好处:
-
可以进行缓存。我们就可以采用动态规划的方法保存中间值,用来代替实际函数的执行结果,大大提升效率。
-
可以进行高并发。因为不依赖于环境,可以调度到另一个线程、worker甚至其它机器上,反正也没有环境依赖。
-
容易测试,容易证明正确性。不容易产生偶现问题,也跟环境无关,非常利于测试。
即使是跟有副作用的代码一起工作,我们也可以在副作用代码中缓存无副作用函数的值,可以将无副作用函数并发执行。测试时也可以更重点关注有副作用的代码以更有效地利用资源。
用函数的组合来替代命令的组合
会写无副作用的函数之后,我们要学习的新问题就是如何将这些函数组合起来。
比如上面的sqr2函数有个问题,如果不是number类型,计算就会出错。按照命令式的思路,我们可能就直接去修改sqr2的代码,比如改成这样:
let sqr2 = function(x){
if (typeof x === ‘number’){
return x * x;
}else{
return 0;
}
}
但是,sqr2的代码已经测好了,我们能不能不改它,只在它外面进行判断?
是的,我们可以这样写:
let isNum = function(x){
if (typeof x === ‘number’){
return x;
}else{
return 0;
}
}
console.log(sqr2(isNum(“20”)));
或者是我们在设计sqr2的时候就先预留出来一个预处理函数的位置,将来要升级就换这个预处理函数,主体逻辑不变:
let sqr2_v3 = function(fn, x){
let y = fn(x);
return y * y;
}
console.log((sqr2_v3(isNum,1.1)));
嫌每次都写isNum烦,可以定义个新函数,把isNum给写死进去:
let sqr2_v4 = function(x){
return sqr2_v3(isNum,x);
}
console.log((sqr2_v4(2.2)));
用容器封装函数能力
现在,我们想重用这个isNum的能力,不光是给sqr2用,我们想给其它数学函数也增加这个能力。
比如,如果给Math.sin计算undefined会得到一个NaN:
console.log(Math.sin(undefined));
这时候我们需要用面向对象的思维了,将isNum的能力封装到一个类中:
class MayBeNumber{
constructor(x){
this.x = x;
}
map(fn){
return new MayBeNumber(fn(isNum(this.x)));
}
getValue(){
return this.x;
}
}
这样,我们不管拿到一个什么对象,用其构造一个MayBeNumber对象出来,再调用这个对象的map方法去调用数学函数,就自带了isNum的能力。
我们先看调用sqr2的例子:
let num1 = new MayBeNumber(3.3).map(sqr2).getValue();
console.log(num1);
let notnum1 = new MayBeNumber(undefined).map(sqr2).getValue();
console.log(notnum1);
我们可以将sqr2换成Math.sin:
let notnum2 = new MayBeNumber(undefined).map(Math.sin).getValue();
console.log(notnum2);
可以发现,输出值从NaN变成了0.
封装到对象中的另一个好处是我们可以用"."多次调用了,比如我们想调两次算4次方,只要在.map(sqr2)之后再来一个.map(sqr2)
let num3 = new MayBeNumber(3.5).map(sqr2).map(sqr2).getValue();
console.log(num3);
使用对象封装之后的另一个好处是,函数嵌套调用跟命令式是相反的顺序,而用map则与命令式一致。
如果不理解的话我们来举个例子,比如我们想求sin(1)的平方,用函数调用应该先写后执行的sqr2,后写先执行的Math.sin:
console.log(sqr2(Math.sin(1)));
而调用map就跟命令式一样了:
let num4 = new MayBeNumber(1).map(Math.sin).map(sqr2).getValue();
console.log(num4);
用 of 来封装 new
封装到对象中,看起来还不错,但是函数式编程还搞出来new对象再map,为什么不能构造对象时也用个函数呢?
这好办,我们给它定义个of方法吧:
MayBeNumber.of = function(x){
return new MayBeNumber(x);
}
下面我们就可以用of来构造MayBeNumber对象啦:
let num5 = MayBeNumber.of(1).map(Math.cos).getValue();
console.log(num5);
let num6 = MayBeNumber.of(2).map(Math.tan).map(Math.exp).getValue();
console.log(num6);
有了of之后,我们也可以给map函数升升级。
之前的isNum有个问题,如果是非数字的话,其实没必要赋给个0再去调用函数,直接返回个0就好了。
之前我们一直没写过箭头函数,顺手写一写:
isNum2 = x => typeof x === ‘number’;
map用isNum2和of改写下:
map(fn){
if (isNum2(this.x)){
return MayBeNumber.of(fn(this.x));
}else{
return MayBeNumber.of(0);
}
}
我们再来看下另一种情况,我们处理返回值的时候,如果有Error,就不处理Ok的返回值,可以这么写:
class Result{
constructor(Ok, Err){
this.Ok = Ok;
this.Err = Err;
}
isOk(){
return this.Err === null || this.Err === undefined;
}
map(fn){
return this.isOk() ? Result.of(fn(this.Ok),this.Err) : Result.of(this.Ok, fn(this.Err));
}
}
Result.of = function(Ok, Err){
return new Result(Ok, Err);
}
console.log(Result.of(1.2,undefined).map(sqr2));
输出结果为:
Result { Ok: 1.44, Err: undefined }
我们来总结下前面这种容器的设计模式:
-
有一个用于存储值的容器
-
这个容器提供一个map函数,作用是map函数使其调用的函数可以跟容器中的值进行计算,最终返回的还是容器的对象
我们可以把这个设计模式叫做Functor函子。
如果这个容器还提供一个of函数将值转换成容器,那么它叫做Pointed Functor.
比如我们看下js中的Array类型:
let aa1 = Array.of(1);
console.log(aa1);
console.log(aa1.map(Math.sin));
它支持of函数,它还支持map函数调用Math.sin对Array中的值进行计算,map的结果仍然是一个Array。
那么我们可以说,Array是一个Pointed Functor。
简化对象层级
有了上面的Result结构了之后,我们的函数也跟着一起升级。如果是数值的话,Ok是数值,Err是undefined。如果非数值的话,Ok是undefined,Err是0:
let sqr2_Result = function(x){
if (isNum2(x)){
return Result.of(x*x, undefined);
}else{
return Result.of(undefined,0);
}
}
我们调用这个新的sqr2_Result函数:
console.log(Result.of(4.3,undefined).map(sqr2_Result));
返回的是一个嵌套的结果:
Result { Ok: Result { Ok: 18.49, Err: undefined }, Err: undefined }
我们需要给Result对象新加一个join函数,用来获取子Result的值给父Result:
join(){
if (this.isOk()) {
return this.Ok;
}else{
return this.Err;
}
}
我们调用的时候最后加上调用这个join:
console.log(Result.of(4.5,undefined).map(sqr2_Result).join());
嵌套的结果变成了一层的:
Result { Ok: 20.25, Err: undefined }
每次调用map(fn).join()两个写起来麻烦,我们定义一个flatMap函数一次性处理掉:
flatMap(fn){
return this.map(fn).join();
}
调用方法如下:
console.log(Result.of(4.7,undefined).flatMap(sqr2_Result));
结果如下:
Result { Ok: 22.090000000000003, Err: undefined }
我们最后完整回顾下这个Result:
class Result{
constructor(Ok, Err){
this.Ok = Ok;
this.Err = Err;
}
isOk(){
return this.Err === null || this.Err === undefined;
}
map(fn){
return this.isOk() ? Result.of(fn(this.Ok),this.Err) : Result.of(this.Ok, fn(this.Err));
}
join(){
if (this.isOk()) {
return this.Ok;
}else{
return this.Err;
}
}
flatMap(fn){
return this.map(fn).join();
}
}
Result.of = function(Ok, Err){
return new Result(Ok, Err);
}
不严格地讲,像Result这种实现了flatMap功能的Pointed Functor,就是传说中的Monad。
偏函数和高阶函数
在前面各种函数式编程模式中对函数的用法熟悉了之后,回来我们总结下函数式编程与命令行编程体感上的最大区别:
-
函数是一等公式,我们应该熟悉变量中保存函数再对其进行调用
-
函数可以出现在返回值里,最重要的用法就是把输入是n(n>2)个参数的函数转换成n个1个参数的串联调用,这就是传说中的柯里化。这种减少了参数的新函数,我们称之为偏函数
-
函数可以用做函数的参数,这样的函数称为高阶函数
偏函数可以当作是更灵活的参数默认值。
比如我们有个结构叫spm,由spm_a和spm_b组成。但是一个模块中spm_a是固定的,大部分时候只需要指定spm_b就可以了,我们就可以写一个偏函数:
const getSpm = function(spm_a, spm_b){
return [spm_a, spm_b];
}
const getSpmb = function(spm_b){
return getSpm(1000, spm_b);
}
console.log(getSpmb(1007));
高阶函数我们在前面的map和flatMap里面已经用得很熟了。但是,其实高阶函数值得学习的设计模式还不少。
比如给大家出一个思考题,如何用函数式方法实现一个只执行一次有效的函数?
不要用全局变量啊,那不是函数式思维,我们要用闭包。
once是一个高阶函数,返回值是一个函数,如果done是false,则将done设为true,然后执行fn。done是在返回函数的同一层,所以会被闭包记忆获取到:
const once = (fn) => {
let done = false;
return function() {
return done ? undefined : ((done=true), fn.apply(this,arguments));
}
}
let init_data = once(
() => {
console.log(“Initialize data”);
}
);
init_data();
init_data();
我们可以看到,第二次调用init_data()没有发生任何事情。
递归与记忆
前面介绍了这么多,但是函数编程其实还蛮复杂的,比如说涉及到递归。
递归中最简单的就是阶乘了吧:
let factorial = (n) => {
if (n===0){
return 1;
}
return n*factorial(n-1);
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数前端工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:前端)
最后
整理面试题,不是让大家去只刷面试题,而是熟悉目前实际面试中常见的考察方式和知识点,做到心中有数,也可以用来自查及完善知识体系。
《前端基础面试题》,《前端校招面试题精编解析大全》,《前端面试题宝典》,《前端面试题:常用算法》PDF完整版点击这里免费领取
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数前端工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
[外链图片转存中…(img-uV4jWXsi-1713719531045)]
[外链图片转存中…(img-wRy0Wu4z-1713719531045)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!
[外链图片转存中…(img-qSBKSwMx-1713719531046)]
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:前端)
[外链图片转存中…(img-uQmtNHWB-1713719531046)]
最后
整理面试题,不是让大家去只刷面试题,而是熟悉目前实际面试中常见的考察方式和知识点,做到心中有数,也可以用来自查及完善知识体系。
《前端基础面试题》,《前端校招面试题精编解析大全》,《前端面试题宝典》,《前端面试题:常用算法》PDF完整版点击这里免费领取
[外链图片转存中…(img-pa0iZx5o-1713719531046)]
[外链图片转存中…(img-FLf1CqFV-1713719531047)]
[外链图片转存中…(img-lDFrGCi7-1713719531047)]