JS基础——高阶函数

定义高阶函数是至少满足下面一个条件的函数:1、接收一个或多个函数作为参数。比如filter函数2、返回一个函数。 比如bind函数举个例子:比如我们要筛数组[1,2,3,4,5]中大于3的所有元素,我们通常的实现方法为:let newArr = [];for(let i = 0,len = arr.length; i < len; i++){ arr[i] > 3 && newArr.push(arr[i])}而使用数组filter方法的话,只需要 let newArr = arr.filter((item) => {return item > 3})。当然我们也可以通过高阶函数来自己实现:Array.prototype.myFilter = function (fn){ let newArr = []; for(let i = 0,len = this.length; i < len; i++){ fn(this[i]) && newArr.push(this[i]) } return newArr;}[1,2,3,4,5].myFilter((item) => { return item > 3})我们可以通过封装高阶函数来复用和简化我们的代码。柯里化柯里化是将一个多参数的函数转换成多个单参数的函数,这个函数会返回一个函数去处理下一个参数。也就是把fn(a,b,c)转换为newFn(a)(b)(c)这种形象。柯里化常见的应用有:参数复用、延迟计算。比如我们有个拼接接口地址的函数:function getUrl(service,context,api){ return service + context + api;}let loginUrl = getUrl(‘http://localhost:8080/’,‘auth’,’/login’) let logoutUrl = getUrl(‘http://localhost:8080/’,‘auth’,’/logout’)每次前两个参数的值都是一样,我们可以柯里化来封装下来达到参数复用:function curry(fn){ let args = Array.prototype.slice.call(arguments,1); return function(){ let innerArgs = Array.prototype.slice.call(arguments); let finalArgs = args.concat(innerArgs); if(finalArgs.length < fn.length){ //fn.length为函数的参数个数 return curry.call(this,fn,…finalArgs) }else{ return fn.apply(null,finalArgs) } }}var getAuthUrl = curry(getUrl,‘http://localhost:8080/’,‘auth’);let loginUrl = getAuthUrl(’/login’)let logoutUrl = getAuthUrl(’/logout’)组合函数组合函数类似于管道,多个函数的执行时,上一个函数的返回值会自动传入到第二个参数继续执行。比如我们替换一个url中的参数:function replaceToken(str){ return str.replace(/{token}/g,‘123455’)}function replaceAccount(str){ return str.replace(/{account}/g,‘xuriliang’)}replaceAccount(replaceToken(‘http://localhost/api/login?token={token}&account={account}’))我们可以利用这种嵌套的写法来实现,但如果嵌套过多,代码可读性就不是很好了。当然我们也可以在一个函数里分过程实现,不过这样函数就不符合单一原则了。利用函数组合我们可以这样写:function compose() { var args = arguments; var start = args.length - 1; return function() { var i = start; var result = args[start].apply(this, arguments); while (i–) result = args[i].call(this, result); return result; }}compose(replaceToken,replaceAccount)(‘http://localhost/api/login?token={token}&account={account}’)组合函数使得我们可以使用一些通用的函数,组合出各种复杂运算。这也是函数编程中pointfree的概念。 ...

March 16, 2019 · 1 min · jiezi

「前端面试题系列6」理解函数的柯里化

前言这是前端面试题系列的第 6 篇,你可能错过了前面的篇章,可以在这里找到:ES6 中箭头函数的用法this 的原理以及用法伪类与伪元素的区别及实战如何实现一个圣杯布局?今日头条 面试题和思路解析最近,朋友T 在准备面试,他为一道编程题所困,向我求助。原题如下:// 写一个 sum 方法,当使用下面的语法调用时,能正常工作console.log(sum(2, 3)); // Outputs 5console.log(sum(2)(3)); // Outputs 5这道题要考察的,就是对函数柯里化的理解。让我们先来解析一下题目的要求:如果传递两个参数,我们只需将它们相加并返回。否则,我们假设它是以sum(2)(3)的形式被调用的,所以我们返回一个匿名函数,它将传递给sum()(在本例中为2)的参数和传递给匿名函数的参数(在本例中为3)。所以,sum 函数可以这样写:function sum (x) { if (arguments.length == 2) { return arguments[0] + arguments[1]; } return function(y) { return x + y; }}arguments 的用法挺灵活的,在这里它则用于分割两种不同的情况。当参数只有一个的时候,进行柯里化的处理。那么,到底什么是函数的柯里化呢?接下来,我们将从概念出发,探究函数柯里化的实现与用途。什么是柯里化柯里化,是函数式编程的一个重要概念。它既能减少代码冗余,也能增加可读性。另外,附带着还能用来装逼。先给出柯里化的定义:在数学和计算机科学中,柯里化是一种将使用多个参数的一个函数转换成一系列使用一个参数的函数的技术。柯里化的定义,理解起来有点费劲。为了更好地理解,先看下面这个例子:function sum (a, b, c) { console.log(a + b + c);}sum(1, 2, 3); // 6毫无疑问,sum 是个简单的累加函数,接受3个参数,输出累加的结果。假设有这样的需求,sum的前2个参数保持不变,最后一个参数可以随意。那么就会想到,在函数内,是否可以把前2个参数的相加过程,给抽离出来,因为参数都是相同的,没必要每次都做运算。如果先不管函数内的具体实现,调用的写法可以是这样: sum(1, 2)(3); 或这样 sum(1, 2)(10); 。就是,先把前2个参数的运算结果拿到后,再与第3个参数相加。这其实就是函数柯里化的简单应用。柯里化的实现sum(1, 2)(3); 这样的写法,并不常见。拆开来看,sum(1, 2) 返回的应该还是个函数,因为后面还有 (3) 需要执行。那么反过来,从最后一个参数,从右往左看,它的左侧必然是一个函数。以此类推,如果前面有n个(),那就是有n个函数返回了结果,只是返回的结果,还是一个函数。是不是有点递归的意思?网上有一些不同的柯里化的实现方式,以下是个人觉得最容易理解的写法:function curry (fn, currArgs) { return function() { let args = [].slice.call(arguments); // 首次调用时,若未提供最后一个参数currArgs,则不用进行args的拼接 if (currArgs !== undefined) { args = args.concat(currArgs); } // 递归调用 if (args.length < fn.length) { return curry(fn, args); } // 递归出口 return fn.apply(null, args); }}解析一下 curry 函数的写法:首先,它有 2 个参数,fn 指的就是本文一开始的源处理函数 sum。currArgs 是调用 curry 时传入的参数列表,比如 (1, 2)(3) 这样的。再看到 curry 函数内部,它会整个返回一个匿名函数。再接下来的 let args = [].slice.call(arguments);,意思是将 arguments 数组化。arguments 是一个类数组的结构,它并不是一个真的数组,所以没法使用数组的方法。我们用了 call 的方法,就能愉快地对 args 使用数组的原生方法了。在这篇 「干货」细说 call、apply 以及 bind 的区别和用法 中,有关于 call 更详细的用法介绍。currArgs !== undefined 的判断,是为了解决递归调用时的参数拼接。最后,判断 args 的个数,是否与 fn (也就是 sum )的参数个数相等,相等了就可以把参数都传给 fn,进行输出;否则,继续递归调用,直到两者相等。测试一下:function sum(a, b, c) { console.log(a + b + c);}const fn = curry(sum);fn(1, 2, 3); // 6fn(1, 2)(3); // 6fn(1)(2, 3); // 6fn(1)(2)(3); // 6都能输出 6 了,搞定!柯里化的用途理解了柯里化的实现之后,让我们来看一下它的实际应用。柯里化的目的是,减少代码冗余,以及增加代码的可读性。来看下面这个例子:const persons = [ { name: ‘kevin’, age: 4 }, { name: ‘bob’, age: 5 }];// 这里的 curry 函数,之前已实现let getProp = curry(function (key, obj) { return obj[key];});const ages = persons.map(getProp(‘age’)); // [4, 5]const names = persons.map(getProp(’name’)); // [‘kevin’, ‘bob’]在实际的业务中,我们常会遇到类似的列表数据。用 getProp 就可以很方便地,取出列表中某个 key 对应的值。另外,为了便于理解调用的写法,可以扩展一下:const names = persons.map(getProp(’name’));等价于:const names = persons.map(item => { return getProp(’name’, item);});最后,来看一个 Memoization 的例子。它用于优化比较耗时的计算,通过将计算结果缓存到内存中,这样对于同样的输入值,下次只需要中内存中读取结果。function memoizeFunction(func) { const cache = {}; return function() { let key = arguments[0]; if (cache[key]) { return cache[key]; } else { const val = func.apply(null, arguments); cache[key] = val; return val; } };}const fibonacci = memoizeFunction(function(n) { return (n === 0 || n === 1) ? n : fibonacci(n - 1) + fibonacci(n - 2);});console.log(fibonacci(100)); // 输出354224848179262000000console.log(fibonacci(100)); // 输出354224848179262000000代码中,第2次计算 fibonacci(100) 则只需要在内存中直接读取结果。总结函数的柯里化,是 Javascript 中函数式编程的一个重要概念。它返回的,是一个函数的函数。其实现方式,需要依赖参数以及递归,通过拆分参数的方式,来调用一个多参数的函数方法,以达到减少代码冗余,增加可读性的目的。虽然一开始理解起来有点云里雾里的,但一旦理解了其中的含义和具体的使用场景,用起来就会得心应手了。PS:欢迎关注我的公众号 “超哥前端小栈”,交流更多的想法与技术。 ...

February 17, 2019 · 2 min · jiezi

Javascript currying柯里化详解

面试题:实现add(1)(2)(3) //结果 = 6,题的核心就是问的js的柯里化先说说什么是柯里化,看过许多关于柯里化的文章,始终搞不太清楚,例如:柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。大多数的blog都是这种说法,说实话我是懵逼的。我的理解是,curry是一个收集参数的方法,收集够了去执行函数。实现前我们先列一下要点1、收集参数(就像面试题多次执行多个参数)是的利用闭包2、每次执行参数有多有少例如add(1)(2,3)(4)3、我们需要知道什么时候参数够了 //如题 //add(1)(2)(3) //逻辑应该是这样add(1)执行收集参数1继续执行收集参数2依次类推直到收集完毕。 function curry(fn) { let arg = []; //用于收集参数 //做一个闭包https://segmentfault.com/a/1190000017824877 return function() { //每执行一次收集一次参数,为什么用concat是因为有时候后是多个参数(2,3) arg = arg.concat([…arguments]); //直到参数收集完成执行fn // 我们需要知道什么时候收集完了,条件就是curry参数fn的参数个数 fn.length //如果收集的参数个数大于等于fn的参数个数执行fn,如果没有递归执行 if (arg.length >= fn.length) { return fn(…arg) } // 参数没有收集完我们需要继续收集,递归 return arguments.callee } } // 测试一下 let testAdd = curry(add1) // console.log(testAdd(1)(2)(3)) // console.log(testAdd(1, 2)(3)) //console.log(testAdd(1)(2, 3))一不小心写完了!不过不能标题党,说好的详解,接下来我们解析一下网上大多数柯里化的实现代码function curry(fn) { function _c(restNum, argsList) { return restNum === 0 ? fn.apply(null, argsList) : function(x) { return _c(restNum - 1, argsList.concat(x)); }; } return _c(fn.length, []);}一眼看不明白没事,我们多看几眼。解析:1、curry也是接收一个参数(fn)这个是必然2、返回了一个函数,接收两个参数,fn.length和一个空数组这个好解释,我写的简版也说过了,fn.length是为了判断参数是否收集够了,参数传一个空数组其实也是闭包的一种实现,用来收集参数。 3、里边是一个三目判断,看着花里胡哨的没那么复杂,判断fn的参数个数如果是0,那就没必要收集了直接执行fn,至于fn.apply(null,argList)我很明白的大声说出来会用个apply就到处用吗?在我看来没有一分钱用,之所以用是因为argList是一个数组,正好apply正好支持第二个参数是数组,主要看起来很牛逼的样子。4、收集参数,fn参数个数不为零,每次收集fn函数参数的个数减一,直到等于0执行fn,这个就没有我写的通用了,我一次传俩就挂了。再来一个例子:bind方法实现Function.prototype.bind = function(context) { //返回一个绑定this的函数,我们需要在此保存this let self = this // 可以支持柯里化传参,保存参数 let arg = […arguments].slice(1) // 返回一个函数 return function() { //同样因为支持柯里化形式传参我们需要再次获取存储参数 let newArg = […arguments] console.log(newArg) // 返回函数绑定this,传入两次保存的参数 //考虑返回函数有返回值做了return return self.apply(context, arg.concat(newArg)) } } // 搞定测试 let fn = Person.say.bind(Person1) fn() fn(18)是的bind方法就是用的柯里化,bind实现详情请移步:https://segmentfault.com/a/11… ...

January 23, 2019 · 1 min · jiezi

手写call、apply、bind及相关面试题解析

它们有什么不同?怎么用?call 接收多个参数,第一个为函数上下文也就是this,后边参数为函数本身的参数。 let obj = { name: “一个” } function allName(firstName, lastName) { console.log(this) console.log(我的全名是“${firstName}${this.name}${lastName}”) } // 很明显此时allName函数是没有name属性的 allName(‘我是’, ‘前端’) //我的全名是“我是前端” this指向window allName.call(obj, ‘我是’, ‘前端’) //我的全名是“我是一个前端” this指向objapplyapply接收两个参数,第一个参数为函数上下文this,第二个参数为函数参数只不过是通过一个数组的形式传入的。allName.apply(obj, [‘我是’, ‘前端’])//我的全名是“我是一个前端” this指向objbindbind 接收多个参数,第一个是bind返回值返回值是一个函数上下文的this,不会立即执行。 let obj = { name: “一个” } function allName(firstName, lastName, flag) { console.log(this) console.log(我的全名是"${firstName}${this.name}${lastName}"我的座右铭是"${flag}") } allName.bind(obj) //不会执行 let fn = allName.bind(obj) fn(‘我是’, ‘前端’, ‘好好学习天天向上’) // 也可以这样用,参数可以分开传。bind后的函数参数默认排列在原函数参数后边 fn = allName.bind(obj, “你是”) fn(‘前端’, ‘好好学习天天向上’)接下来搓搓手实现call、apply和bind实现call let Person = { name: ‘Tom’, say() { console.log(this) console.log(我叫${this.name}) } } // 先看代码执行效果 Person.say() //我叫Tom Person1 = { name: ‘Tom1’ } // 我们尝试用原生方法call来实现this指向Person1 Person.say.call(Person1) //我叫Tom1通过第一次打印执行和第二次打印执行我发现,如果Person1有say方法那么Person1直接执行Person1.say() 结果就是我是Tom1,是的call就是这么实现的。再看代码 Function.prototype.MyCall = function(context) { //context就是demo中的Person1 // 必须此时调用MyCall的函数是say方法,那么我们只需要在context上扩展一个say方法指向调用MyCall的say方法这样this console.log(this) context.say = this //Mycall里边的this就是我们虚拟的say方法 context.say() } // 测试 Person.say.MyCall(Person1)//我叫Tom1perfect!爆棚的满足感!不过拿脚趾头想想也不会这么简单,继续完善我们自己找茬1、call支持多个参数,有可能一个也不没有2、考虑多参数时要把参数传给扩展方法。3、给上下文定义的函数要保持唯一不能是say4、扩展完我们需要吧自定义函数删除接下来针对找茬问题一一解决 let Person = { name: ‘Tom’, say() { console.log(this) console.log(我叫${this.name}) } } Person1 = { name: ‘Tom1’ } //如果没有参数 Person.say.call()没有指定this,this指向window我们也要这样 Function.prototype.MyCall = function(context) { // 如果没有参数我们参考call的处理方式 context = context || window //context就是demo中的Person1 // 必须此时调用MyCall的函数是say方法,那么我们只需要在context上扩展一个say方法指向调用MyCall的say方法这样this context.say = this //Mycall里边的this就是我们虚拟的say方法 context.say() } Person.say.MyCall()没毛病!继续解决// 找茬2:我们默认定义context.say = this fn如果已经被占用 嘎嘎 sb了。 不怕 搞定它 // say需要是一个唯一值 是不是突然想到es6的新类型 Symbol fn = Symbol() 不过我们装逼不嫌事大 都说自己实现了 function mySymbol(obj) { // 不要问我为什么这么写,我也不知道就感觉这样nb let unique = (Math.random() + new Date().getTime()).toString(32).slice(0, 8) // 牛逼也要严谨 if (obj.hasOwnProperty(unique)) { return mySymbol(obj) //递归调用 } else { return unique } }//接下来我们一并把多参数和执行完删除自定义方法删除掉一块搞定 Function.prototype.myCall1 = function(context) { // 如果没有传或传的值为空对象 context指向window context = context || window let fn = mySymbol(context) context.fn = this //给context添加一个方法 指向this // 处理参数 去除第一个参数this 其它传入fn函数 let arg = […arguments].slice(1) //[…xxx]把类数组变成数组,arguments为啥不是数组自行搜索 slice返回一个新数组 context.fn(…arg) //执行fn delete context.fn //删除方法 } let Person = { name: ‘Tom’, say(age) { console.log(this) console.log(我叫${this.name}我今年${age}) } } Person1 = { name: ‘Tom1’ } Person.say.call(Person1,18)//我叫Tom1我今年18测试结果相当完美!实现apply接下来apply就简单多了,只有多参数时第二个参数是数组,就不一步步细说了。 Function.prototype.myApply = function(context) { // 如果没有传或传的值为空对象 context指向window if (typeof context === “undefined” || context === null) { context = window } let fn = mySymbol(context) context.fn = this //给context添加一个方法 指向this // 处理参数 去除第一个参数this 其它传入fn函数 let arg = […arguments].slice(1) //[…xxx]把类数组变成数组,arguments为啥不是数组自行搜索 slice返回一个新数组 context.fn(arg) //执行fn delete context.fn //删除方法 }实现bind这个和call、apply区别还是很大的,容我去抽根烟回来收拾它还是老套路先分析bind都能干些什么,有什么特点 1、函数调用,改变this 2、返回一个绑定this的函数 3、接收多个参数 4、支持柯里化形式传参 fn(1)(2) Function.prototype.bind = function(context) { //返回一个绑定this的函数,我们需要在此保存this let self = this // 可以支持柯里化传参,保存参数 let arg = […arguments].slice(1) // 返回一个函数 return function() { //同样因为支持柯里化形式传参我们需要再次获取存储参数 let newArg = […arguments] console.log(newArg) // 返回函数绑定this,传入两次保存的参数 //考虑返回函数有返回值做了return return self.apply(context, arg.concat(newArg)) } } // 搞定测试 let fn = Person.say.bind(Person1) fn() fn(18)是的,完美,实现了绑定this,返回函数,不立即执行,可以柯里化形式传参。简版的实现就算完成了欢迎吐槽or点赞! ...

January 18, 2019 · 2 min · jiezi