本文转载自微信公众号《前端下午茶》作者SHERlocked93。转载本文请联系前端下午茶公众号。“在JavaScript中,函数是一等公民”,我们总能在各种书籍和文章中看到这句话。既然有头等舱,那当然也有二等舱。如果把公民分等级,一等公民什么都能做,二等公民就不能做这个做那个。JavaScript函数也是对象,可以有属性,可以赋值给变量,可以作为元素放在数组中,可以作为其他对象的属性,可以做任何事情。它可以做其他对象可以做但其他对象不能做的事情。它可以做它做的事。这不就是一等公民的身份吗?——ChengMoMorgan所以它的意思是:函数和其他普通对象一样,上面都有属性和方法。普通对象能做到的,函数也能做到。由于JavaScript的极大自由度,函数被赋予了出色的表现力和灵活性,但同时也带来了很多令人头疼的问题。在本文中,我们将讨论两个最常遇到的与函数密切相关的概念:闭包和高阶函数。这两个概念在后面的设计模式文章中会经常遇到。注:本文为基础文章。如果你已经对本文的相关知识点有了很好的了解,可以跳过本文。如果你还不够了解,或者理解不全面,可以通过本文复习一下~1.闭包1.1什么是闭包当一个函数能够记住并访问它所在的词法作用域时,就会产生一个闭包,即使该函数是在当前词法作用域之外执行的。我们先看一个闭包的例子:functionfoo(){vara=2functionbar(){console.log(a)}returnbar}varbaz=foo()baz()//Output:2foo函数传递一个函数bar,传递的bar被分配给baz并被调用。虽然此时baz是在foo的作用域之外执行的,但是baz在调用时可以访问到之前bar函数所在的foo的内部作用域。由于bar是在foo函数内部声明的,bar有一个闭包覆盖了foo的内部作用域,这样foo的内部作用域就一直活着,不会被回收。一般来说,一个函数的整个内部作用域在执行后都会被销毁,因为JavaScript的GC(GarbageCollection)垃圾回收机制会自动回收未使用的内存空间。但是闭包会阻止一些GC,比如本例中的foo(),因为返回的bar函数仍然持有对其作用域的引用,所以它的内部作用域不会被回收。注意:如果您不必使用闭包,请尽量避免创建它,因为闭包在处理速度和内存消耗方面会对性能产生负面影响。1.2使用闭包实现结果缓存(备忘录模式)备忘录模式是应用闭包特性的典型应用。比如有一个函数:functionadd(a){returna+1;}当多次运行add()时,每次得到的结果都会重新计算。如果是开销很大的计算操作,会消耗更多的性能。这里可以为已经计算好的输入做一个缓存。所以这里可以利用闭包的特性来实现一个简单的缓存,在函数内部使用一个对象来存储输入参数。如果下次输入相同的参数,则比较对象的属性。如果有缓存,就直接从这个对象中取值。/*内存函数*/functionmemorize(fn){varcache={}returnfunction(){varargs=Array.prototype.slice.call(arguments)varkey=JSON.stringify(args)returncache[key]||(cache[key]=fn.apply(fn,args))}}/*复杂的计算函数*/functionadd(a){returna+1}varadder=memorize(add)adder(1)//output:2current:cache:{'[1]':2}adder(1)//输出:2current:cache:{'[1]':2}adder(2)//输出:3current:cache:{'[1]':2,'[2]':3}使用ES6会更优雅:/*memorizefunction*/functionmemorize(fn){constcache={}returnfunction(...args){constkey=JSON.stringify(args)returncache[key]||(cache[key]=fn.apply(fn,args))}}/*复杂计算函数*/functionadd(a){returna+1}constadder=memorize(add)adder(1)//输出:2当前:cache:{'[1]':2}adder(1)//输出:2current:cache:{'[1]':2}adder(2)//输出:3current:cache:{'[1]':2,'[2]':3}稍微解释一下:memo函数中使用JSON.stringify将传递给adder函数的参数序列化为字符串,作为i缓存的索引。将add函数的结果作为索引值传递给缓存,这样如果传递的参数在adder运行之前就已经传完了,那么就直接返回缓存的计算结果,不再继续计算。如果传入的参数没有计算过,则计算并缓存fn.apply(fn,args),并返回计算结果。当然这里的实现如果要应用到实际中,还需要进一步完善。比如:缓存不能永远扩容,内存资源消耗太大。我们只能缓存最新的n个传入的;在浏览器中使用时,我们可以使用浏览器的持久化手段来持久化缓存,比如cookie、localStorage等;这里的复杂计算函数可以是过去的某个状态,比如对某个目标的操作,这样过去的状态就缓存起来,方便状态回滚。复杂的计算函数也可以是返回时间相对较慢的异步操作。这样如果缓存了结果,下次就可以直接从本地获取,而不用重新做异步请求。注意:缓存不能是Map,因为Map的键是用===比较的,所以当传入一个引用类型的值作为键时,虽然看起来相等,但实际上并不相等,比如[1]!==[1],所以它仍然存储为不同的密钥。//X错误演示函数memorize(fn){constcache=newMap()returnfunction(...args){returncache.get(args)||cache.set(args,fn.apply(fn,args)).get(args)}}functionadd(a){returna+1}constadder=memorize(add)adder(1)//2cache:{[1]=>2}adder(1)//2cache:{[1]=>2,[1]=>2}加法器(2)//3缓存:{[1]=>2,[1]=>2,[2]=>3}2.高阶函数高阶函数是输入参数一个函数,或者输出是一个函数的函数。2.1函数作为参数如果你用过setTimeout、setInterval、ajax请求,那么你就用过高阶函数,也就是我们看到的最常见的场景:回调函数,因为它将一个函数作为参数传递给另一个函数。比如在ajax请求中,我们通常使用回调函数来定义请求成功或失败时的操作逻辑:$.ajax("/request/url",function(result){console.log("请求是successful!")})inArray、Object、String等基础对象的原型上有很多操作方法,可以接受回调函数,方便地进行对象操作。这里有一个非常常用的Array.prototype.filter()方法,它返回一个新创建的数组,其中包含执行回调函数后返回true或true值的所有数组元素。varwords=['spray','limit','elite','exuberant','destruction','present'];varresult=words.filter(function(word){returnword.length>6})//输出:["exuberant","destruction","present"]回调函数的另一个应用是hook。如果你使用过Vue或React等框架,那么你应该对hook很熟悉。它的形式是这样的:functionfoo(callback){//...一些操作callback()}2.2函数作为返回值另一种常见的高阶函数场景是在一个函数内部输出另一个函数,例如:functionfoo(){returnfunctionbar(){}}主要是用闭包保持作用域:functionadd(){varnum=0returnfunction(a){returnnum=num+a}}varadder=add()adder(1)//Output:1adder(2)//Output:31.CurryingCurrying,也称为PartialEvaluation,就是将原来接受多个参数的函数,转化为接受单个参数(原函数的第一个参数)的函数,并返回一个新的函数,即新函数可以接受剩余的参数,最后返回与原函数相同的结果。其核心思想是将多个参数传入的函数拆分成一个单(或部分)参数函数,返回内部调用下一个单(或部分)参数函数,依次处理剩下的参数。Currying共有三个常用功能:参数多路复用提前返回,延迟计算/运行我们来看看currying的一般实现://ES5methodfunctioncurrying(fn){varrest1=Array.prototype.slice.call(arguments)rest1.shift()returnfunction(){varrest2=Array.prototype.slice.call(arguments)returnfn.apply(null,rest1.concat(rest2))}}//ES6方法函数currying(fn,...rest1){returnfunction(...rest2){returnfn.apply(null,rest1.concat(rest2))}}用它来柯里化一个sayHello函数并尝试一下://connecttheabovefunctionsayHello(name,age,fruit){console.log(console.log(`我叫${name},我${age}岁,我喜欢吃${fruit}`))}varcurryingShowMsg1=currying(sayHello,'小明')curryingShowMsg1(22,'Apple')//输出:我叫小明,今年22岁,我喜欢吃苹果varcurringShowMsg2=currying(sayHello,'小白',20)curryingShowMsg2('西瓜')//输出:我叫小白,我今年20岁,我喜欢吃西瓜更高级的用法见:JavaScript函数式编程技巧-Currying2.反柯里化Currying就是固定一些参数,返回一个接受剩余参数的函数,也称为a部分计算功能。目的是缩小应用范围,创建更有针对性的功能。核心思想是将多个参数传入的函数拆分成一个单参数(或部分)函数,然后返回内部调用下一个单参数(或部分)函数,依次处理剩下的参数。从字面上看,anti-currying与functioncurrying具有相反的含义和用法,扩大了应用范围,创建了一个应用范围更广的函数。将只适用于特定对象的方法扩展到更多的对象。来看看反柯里化的大致实现吧~//ES5方法Function.prototype.unCurrying=function(){varself=thisreturnfunction(){varrest=Array.prototype.slice.call(arguments)returnFunction.prototype.call.apply(self,rest)}}//ES6方式Function.prototype.unCurrying=function(){constself=thisreturnfunction(...rest){returnFunction.prototype.call.apply(self,rest)}}如果你认为把函数放在Function的原型上不太好,也可以这样://ES5方式functionunCurrying(fn){returnfunction(tar){varrest=Array.prototype.slice.call(arguments)rest.shift()returnfn.apply(tar,rest)}}//ES6方法functionunCurrying(fn){returnfunction(tar,...argu){returnfn.上面的push方法借给类数组添加一个元素,比如arguments://Continuedfromtheabovevarpush=unCurrying(Array.prototype.push)functionexecPush(){push(arguments,4)console.log(arguments)}execPush(1,2,3)//Output:[1,2,3,4]简单来说,函数柯里化就是降低高阶函数的阶数,缩小应用范围,创建更有针对性的函数。函数(arg1,arg2)//=>函数(arg1)(arg2)函数(arg1,arg2,arg3)//=>函数(arg1)(arg2)(arg3)函数(arg1,arg2,arg3,arg4)//=>function(arg1)(arg2)(arg3)(arg4)function(arg1,arg2,...,argn)//=>function(arg1)(arg2)...(argn)反柯里化是反过来,增加应用范围,让方法使用场景更大。使用反柯里化,可以借用本地方法,允许任何对象拥有本地对象方法。obj.func(arg1,arg2)//=>func(obj,arg1,arg2)可以这样理解柯里化和反柯里化的区别:柯里化就是在运行前提前传递参数,可以传递多个参数;反柯里化就是延迟参数传递。在运行过程中,将原来固定的参数或者thiscontext作为参数延迟到未来。更高级的用法见:JavaScript函数式编程技巧-反柯里化3.分部函数分部函数是创建一个函数调用另一部分(参数或变量是预制好的函数)。生成一个实际执行的函数。它不包括我们真正需要的逻辑代码。它只是根据传入的参数返回其他函数。返回的函数有真正的处理逻辑,如:varisType=function(type){returnfunction(obj){returnObject.prototype。toString.call(obj)===`[object${type}]`}}varisString=isType('String')varisFunction=isType('Function')这样判断对象类型的一组方法就是使用偏函数方法快速创建~偏函数与柯里化的区别:柯里化是将一个接受n个参数的函数从原来的一次传递所有参数并执行,改为多次接受参数然后执行,对于示例:add=(x,y,z)=>x+y+z→curryAdd=x=>y=>z=>x+y+z;部分函数固定了函数的某一部分,由传递的参数或方法返回一个新的函数来接受剩余的参数,数量可以是一个也可以是多个;当一个柯里化函数只接受两个参数时,比如curry()(),此时的柯里化函数和偏函数的概念是类似的,偏函数可以认为是柯里化函数的退化版本。原文链接:https://mp.weixin.qq.com/s/pv-EYBLpzwuC9Vkkt9gNZw
