听风是风

学或不学,知识都在那里,只增不减。

导航

精读JavaScript模式(六),Memoization模式与函数柯里化的应用

假期就这么结束了!十天假就有三天在路上,真的难受!想想假期除了看了两场电影貌似也没做什么深刻印象的事情。流浪地球,特效还是很赞,不过对于感情的描写还是逃不掉拖沓和尴尬的通病,对于国产科幻还是抱有支持的态度。疯狂的外星人相比读大学期间看的疯狂的赛车,荒诞感还是差了点,也许是我笑点太高...不过整体还是感觉比流浪地球值票价,个人观点吧。

开年来同事说自己小舅子年终奖税后18W,他这两天深受打击,我听完也深受打击,哎。

假期结束也该好好安排下今年的时间了,年底辞职的规划不变,加上未来几年要结婚,想想还有很多东西没学习压力还是很大,为了多挣点钱加油吧!

 一、Memoization模式

 函数是对象,我们可以为函数添加属性,或者读取它自带的属性,比如函数都有一个length属性,此属性表示函数参数的长度。例如:

function demo (a, b, c) {};
console.log(demo.length);//3

为函数添加自定义属性有一个很好用的场景就是缓存函数运行结果(返回值),这样下次执行时就不用重复的去执行那些复杂的计算,这种做法就是在编程中常用的Memoization模式。

const myFunc = function (param) {
    let result;
    result = param * 2;
    if (!myFunc.cache[param]) {
        myFunc.cache[param] = result;
    };
};
myFunc.cache = {};
//调用函数
myFunc(1);
console.log(myFunc.cache); //{1: 2}
myFunc(2);
console.log(myFunc.cache); //{1: 2, 2: 4}
//直接获取对应形参的执行结果
console.log(myFunc.cache[1]);//2 7 4

在上述代码中我们定义了一个myFunc函数,它主要接受一个参数param,并做将参数乘以2的操作,这里先不考虑param不为number类型的情况。

同时我们为myFunc添加了一个cache属性,是一个对象,每次函数调用执行,参数都会成为cache属性的key,函数运算结果会成为cache对应的value,这样就缓存了每次运算的键值对运算结果。

假设运算,或者逻辑远比*2复杂,那么当我们下载又想知道当传入参数2的结果时,只需要通过myFunc.cache[2]访问对应的value即可,避免的重复的复杂运算。

二、配置对象(函数形参对象写法)

正常开发中程序的维护以及修改是无法避免的问题,比如一个简单的需求,随着时间的推移,越来越多的功能被加进来。

假设我们有一个负责添加联系人的函数,接受姓和名两个参数。

function addPerson (first, last) {}

现在需求变了,除了姓名,你还得保存性别,地址等可选信息,于是我们给函数形参添加了2个参数,因为是可选,我们还得将参数放在后面一点。

function addPerson (first, last, gender, address) {}

当我们调用时,我们得保证参数传递顺序与函数形参一致,且必要的形参写在前面。

addPerson("echo", 'lun', null, '深圳');

这样就是比较麻烦的,好的做法就是将函数形参替换为一个参数,且此参数是一个对象,我们称之为conf,也就是配置英文单词的缩写。

function addPerson(param) {
    let userName = param.first + param.last,
        userAddress = param.address;
};
let conf = {
    gender: '',
    first: 'echo',
    address: '深圳',
    last: 'lun'
};
addPerson(conf);

这样做的好处是,调用者不需要记住参数的顺序,可以更方便的跳过可选参数,由于形参变少,提升可读性的同时,也增加了可维护性,毕竟增删参数更加简单。

但缺点是,需要记住参数的名称,毕竟函数内部是通过参数作为key来访问的,其次是参数名不可压缩。此模式对于修改CSS样式的函数非常实用,因为CSS样式一般较多,并且存在可选属性。

三、函数柯里化

1.函数应用

如何使用一个函数,最为直观的是函数调用,但在一些纯粹的函数式编程中,对函数的描述不是被调用,而是被应用。

在JS中,函数除了调用,我们也能通过Function.prototype.aplly()来应用一个函数。因为函数本身就是对象,它们也有自己的方法

let sayHi = function (who) {
    return "hello" + (who ? "," + who : "") + "!";
};
//函数调用
sayHi(); //hello!
sayHi('echo') //hello,echo!
//函数应用
sayHi.apply(null, ["时间跳跃"]);//hello,时间跳跃!

上述代码中不管调用或是应用函数,都能得到预期的结果。在函数应用中,apply()接受两个参数,第一个参数是函数内部this所绑定的对象,第二个参数是一个参数数组,参数数组在函数内部会变成类数组arguments对象。如果第一个参数为null,那么this会指向全局对象。

但如果一个函数是一个对象的方法时,第一个参数不会传递null,这样做的目的是保证方法中的this绑定到一个有效对象,在下方代码中,this指向了alien。

let alien = {
    sayHi: function (who) {
        console.log("hello" + (who ? "," + who : "") + "!"); 
    }
};

//函数调用
alien.sayHi('echo') //hello,echo!
//函数应用
alien.sayHi.apply(alien, ["时间跳跃"]); //hello,时间跳跃!

事实上函数调用属于函数应用的一种语法糖,函数应用出了apply()之外,还有提供一个call()方法,但它仍然只是apply()的一种语法糖。

apply()与call()两者的区别在于,apply()只接受两个参数,第一个为函数this绑定的对象,第二参数为函数参数数组。相比之下,call()可接受任意多的参数,第一个参数为this绑定对象,从二个参数开始将依次传递给函数。

假设函数只有一个参数,使用call()要比apply()要更加优化,因为这样可以省去一个创建数组的步骤。

 2.函数部分应用

现在我们可以说,调用一个函数本质是给函数应用了一堆参数,假设有一个处理两数相加的函数,我们其实可以按函数应用的思想将步骤拆分出来。

function add(x, y) {
  return x + y;
};
add(5, 1);
//按函数应用的思想拆分运行步骤
//第一步
function add(5, y){
    return 5 + y;
};
// 第二步
function add(5, 4){
    return 5 + 4;
};

虽然上述代码中步骤1与2并不是真正有效的代码,但大概表达了这个意思。先应用了参数5,替换了函数内部变量,然后重复此过程,直到替换所有参数,计算得到最终结果。

步骤1中我们只应用了参数5,并未得到最终计算结果,反而得到了一个替换了部分参数的另一个函数,我们可以称此函数为add函数的部分应用(partialApply)。

假设现在我们有一个虚拟的部分应用函数partialApply(),来看一段代码

let add = function(x, y) {
  return x + y;
};
// 正常的函数应用
add.apply(null, [5, 4]); //9
//部分应用拆分
let newAdd = add.partialApply(null, [5]);
newAdd.apply(null,[4]);//9

函数应用是分步骤的,我们可以说调用函数add(5,4)的写法是add(5)(4)一种语法糖,本质是相同的。

当然由于上述代码中partialApply()函数是我们假想的,add(5)(4)这样的写法也是会报错的,这里只是为了传达一个概念,让函数理解并且处理部分应用的过程叫柯里化(Currying)。

3.函数柯里化

柯里化是一个变换函数的过程,还是上面add函数,我们尝试修改下,实现我们想要的柯里化。

let add = function(x, y) {
  let oldX = x,
      oldY = y;
    if(oldY === undefined){
        return function (newY) {
            return oldX + newY;
        };
    };
    return x + y;
};
//普通调用
add(5,4)//9
//分步骤调用
add(5)(4);//9

实现上利用了闭包的思想,当调用add(5)时我们其实得到了一个全新的函数,第二次调用add(4)时得到了最终结果,当然这个实现有点尴尬,内部还创建了三个意义不大的变量oldX,oldY与newY,只是为了方便理解,我们改改。

let add = function(x, y) {
    if(y === undefined){
        return function (y) {
            return x + y;
        };
    };
    return x + y;
};
//普通调用
add(5,4)//9
//分步骤调用
add(5)(4);//9

 这样就精简了很多,也达到了我们需要的效果。但是有个问题,这样的写法不够复用,仅仅是写在了add函数内部供add自身使用,我们能不能封装出一个较为共用的方法,让任意函数都能柯里化呢。

//定义通用的柯里化函数
function schonfinkelize(fn) {
  var slice = Array.prototype.slice,
    stored_args = slice.call(arguments, 1);
  return function() {
    var new_args = slice.call(arguments),
      args = stored_args.concat(new_args);
    return fn.apply(null, args);
  };
};
//定义add函数
function add(x, y) {
  return x + y;
};
//开始调用
let newAdd = schonfinkelize(add, 5);
newAdd(4);//9
schonfinkelize(add, 5)(4);//9

在上述代码中,我们定义了一个通用的柯里化函数schonfinkelize,为什么叫这个名,书上说难发音,难记,有代表性....

在schonfinkelize中,我们通过Array.prototype.slice.call()方法将类数组arguments转为数组,在第一次调用时我们传递了add与数字5两个参数,将5存入变量stored_args的同时,并返回了一个全新的函数。

在第二次调用时我们传递了参数4,通过数组concat方法,将前后两次的参数合并成了新数组[5,4]作为了fn,也就是add函数运行时所需要的函数。

4.什么时候使用柯里化

看到这问题就来了,这不神经病吗,好好的调用干嘛非得拆成几步,意义在哪呢?我们什么情况下要使用函数柯里化呢?存在即合理,当我们调用一个函数多次,传入的参数大部分相同时,我们就可以通过传递一部分的参数动态创建一个新的函数,这个新函数会存储那些重复的参数,这样你就不用反复的传入,执行相同的步骤,然后再在此函数的基础上补全别的参数,达到最终执行的目的。

假设还是这个add函数,计算两数之和,我们要分别计算3+4和3+5的合,利用柯里化,我们可以将参数3的状态形成一个新的函数,在分别补全4和5的参数,得到两次计算的结果。(看到这我也是惊叹了)

let newAdd = schonfinkelize(add, 3);
newAdd(4)//7
newAdd(5)//8

那么到这里,函数这一章节就算读完了,这里做个简单的小总结:

函数是一等公民,它可以作为值传递,可以作为函数的返回值,也可以拥有属性和方法。函数拥有本地作用域,而大括号不产生会计作用域。

创建一个函数有三种方法,函数声明,函数表达式,以及构造函数new创建。

在介绍完函数后,也提到了函数的一些有趣的模式,比如回调模式,将函数作为参数传递给另一个函数,而对于函数的参数控制,我们也说了配置对象,通过对象的写法,让函数的参数更易可读与维护。函数可以返回一个函数,比如闭包,最后我们介绍了函数应用与函数柯里化。

初始化模式是我们在日常开发中一种干净结构化的方法,例如立即执行函数,达到初始化的目的,也避免了污染全局。而初始化我们大部分希望只执行一次,不需要反复的调用,所以我们也介绍了条件初始化。

最后我们介绍了一些性能模式,比如Memoization模式来记忆复杂的运算结果,提高代码的执行效率。自定义函数的重写模式,让第二次使用时能做更少的工作等等。

从这章开始,提到的一些东西我也觉得渐渐有趣起来了,下一章节是关于对象的创建模式,反正这两个月把这本书读完记录完,加油吧。

posted on 2019-02-15 00:00  听风是风  阅读(624)  评论(0编辑  收藏  举报