Nice to meet you!css
能来到这里就表明已经看过上一篇关于并不孤独的闭包文章了(没看不能来?固然......能够来),那么就不废话了,先来看下什么是高阶函数html
下面看个简单的demo:前端
说实在的原本只是个简单的🌰,不过越写越兴奋,就弄成了个小demo了,你们也能够copy下去本身添油加醋一下(写成各类版本),乐呵一下吧,PS:因为代码过多占用文章,将css样式去掉了,样式的实现你们随意发挥就行了node
<body> <div id="box" class="clearfix"></div> <script src="https://cdn.bootcss.com/jquery/1.12.4/jquery.min.js"></script> <script src="./index.js"></script> </body> 复制代码
js部分jquery
// index.js // 回调函数 // 异步请求 let getInfo = function (keywords, callback) { $.ajax({ url: 'http://musicapi.leanapp.cn/search', // 以网易云音乐为例 data: { keywords }, success: function (res) { callback && callback(res.result.songs); } }) }; $('#btn').on('click', function() { let keywords = $(this).prev().val(); $('#loading').show(); getInfo(keywords, getData); }); // 加入回车 $("#search_inp").on('keyup', function(e){ if (e.keyCode === 13) { $('#loading').show(); getInfo(this.value, getData); } }); function getData(data) { if (data && data.length) { let html = render(data); // 初始化Dom结构 initDom(html, function(wrap) { play(wrap); }); } } // 格式化时间戳 function formatDuration(duration) { duration = parseInt(duration / 1000); // 转换成秒 let hour = Math.floor(duration / 60 / 60), min = Math.floor((duration % 3600) / 60), sec = duration % 60, result = ''; result += `${fillIn(min)}:${fillIn(sec)}`; return result; } function fillIn(n) { return n < 10 ? '0' + n : '' + n; } let initDom = function (tmp, callback) { $('.item').remove(); $('#loading').hide(); $('#box').append(tmp); // 这里由于不知道dom合适才会被彻底插入到页面中 // 因此用callback当参数,等dom插入后再执行callback callback && callback(box); }; let render = function (data) { let template = ''; let set = new Set(data); data = [...set]; // 能够利用Set去作下简单的去重,可忽略这步 for (let i = 0; i < 8; i++) { let item = data[i]; let name = item.name; let singer = item.artists[0].name; let pic = item.album.picUrl; let time = formatDuration(item.duration); template += ` <div class="item"> <div class="pic" data-time="${time}"> <span></span> <img src="${pic}" /> </div> <h4>${name}</h4> <p>${singer}</p> <audio src="http://music.163.com/song/media/outer/url?id=${item.id}.mp3"></audio> </div>`; } return template; }; let play = function(wrap) { wrap = $(wrap); wrap.on('click', '.item', function() { let self = $(this), $audio = self.find('audio'), $allAudio = wrap.find('audio'); for (let i = 0; i < $allAudio.length; i++) { $allAudio[i].pause(); } $audio[0].play(); self.addClass('play').siblings('.item').removeClass('play'); }); }; 复制代码
按照上面的代码啪啪啪,就会获得下面这样的效果,一块儿来看下吧ajax
不过依然感谢网易云音乐提供的API接口,让咱们聆听美妙好音乐编程
亲们,函数做为返回值输出的应用场景那就太多了,这也体现了函数式编程的思想。其实从闭包的例子中咱们就已经看到了关于高阶函数的相关内容了,哈哈设计模式
还记得在咱们去判断数据类型的时候,咱们都是经过Object.prototype.toString来计算的。每一个数据类型之间只是'[object XXX]'不同罢了api
因此在咱们写类型判断的时候,通常都是将参数传入函数中,这里我简单写一下实现,我们先来看看数组
function isType(type) { return function(obj) { return Object.prototype.toString.call(obj) === `[object ${type}] } } const isArray = isType('Array'); const isString = isType('String'); console.log(isArray([1, 2, [3,4]]); // true console.log(isString({}); // false 复制代码
其实上面实现的isType函数,也属于偏函数的范畴,偏函数其实是返回了一个包含预处理参数的新函数,以便以后能够调用
另外还有一种叫作预置函数,它的实现原理也很简单,当达到条件时再执行回调函数
function after(time, cb) { return function() { if (--time === 0) { cb(); } } } // 举个栗子吧,吃饭的时候,我很能吃,吃了三碗才能吃饱 let eat = after(3, function() { console.log('吃饱了'); }); eat(); eat(); eat(); 复制代码
上面的eat函数只有执行3次的时候才会输出'吃饱了',仍是比较形象的。
这种预置函数也是js中巧妙的装饰者模式的实现,装饰者模式在实际开发中也很是有用,再之后的岁月里我也会好好研究以后分享给你们的
好了,不要停,不要停,再来看一个栗子
// 这里咱们建立了一个单例模式 let single = function (fn) { let ret; return function () { console.log(ret); // render一次undefined,render二次true,render三次true // 因此以后每次都执行ret,就不会再次绑定了 return ret || (ret = fn.apply(this, arguments)); } }; let bindEvent = single(function () { // 虽然下面的renders函数执行3次,bindEvent也执行了3次 // 可是根据单例模式的特色,函数在被第一次调用后,以后就再也不调用了 document.getElementById('box').onclick = function () { alert('click'); } return true; }); let renders = function () { console.log('渲染'); bindEvent(); } renders(); renders(); renders(); 复制代码
这个高阶函数的栗子,能够说一石二鸟啊,既把函数当作参数传递了,又把函数当返回值输出了。
单例模式也是一种很是实用的设计模式,在之后的文章中也会针对这些设计模式去分析的,敬请期待,哈哈,下面再看看高阶函数还有哪些用途
柯里化又称部分求值,柯里化函数会接收一些参数,而后不会当即求值,而是继续返回一个新函数,将传入的参数经过闭包的形式保存,等到被真正求值的时候,再一次性把全部传入的参数进行求值
还能阐述的更简单吗?在一个函数中填充几个参数,而后再返回一个新函数,最后进行求值,没了,是否是说的简单了
说的再简单都不如几行代码演示的清楚明白
// 普通函数 function add(x,y){ return x + y; } add(3,4); // 7 // 实现了柯里化的函数 // 接收参数,返回新函数,把参数传给新函数使用,最后求值 let add = function(x){ return function(y){ return x + y; } }; add(3)(4); // 7 复制代码
以上代码很是简单,只是起个引导的做用。下面咱们来写一个通用的柯里化函数
function curry(fn) { let slice = Array.prototype.slice, // 将slice缓存起来 args = slice.call(arguments, 1); // 这里将arguments转成数组并保存 return function() { // 将新旧的参数拼接起来 let newArgs = args.concat(slice.call(arguments)); return fn.apply(null, newArgs); // 返回执行的fn并传递最新的参数 } } 复制代码
实现了通用的柯里化函数,了不得啊,各位很了不得啊。
不过这还不够,咱们还能够利用ES6再来实现一下,请看以下代码:
// ES6版的柯里化函数 function curry(fn) { const g = (...allArgs) => allArgs.length >= fn.length ? fn(...allArgs) : (...args) => g(...allArgs, ...args) return g; } // 测试用例 const foo = curry((a, b, c, d) => { console.log(a, b, c, d); }); foo(1)(2)(3)(4); // 1 2 3 4 const f = foo(1)(2)(3); f(5); // 1 2 3 5 复制代码
两种不一样的实现思路相同,以后能够试着分析一下
不过你们有没有发现咱们在ES5中使用的bind方法,其实也利用了柯里化的思想,那么再来看一下下
let obj = { songs: '以父之名' }; function fn() { console.log(this.songs); } let songs = fn.bind(obj); songs(); // '以父之名' 复制代码
为何这么说?这也看不出什么头绪啊,别捉急,再来看一下bind的实现原理
Function.prototype.bind = function(context) { let self = this, slice = Array.prototype.slice, args = slice.call(arguments); return function() { return self.apply(context, args.slice(1)); } }; 复制代码
是否是似曾相识,是否是,是否是,有种师出同门的赶脚了啊
啥?反柯里化,刚刚被柯里化弄的手舞足蹈的,如今又出现了个反柯里化,有木有搞错啊!那么反柯里化是什么呢?简而言之就是函数的借用,天下函数(方法)你们用
好比,一个对象未必只能使用它自身的方法,也能够去借用本来不属于它的方法,要实现这点彷佛就很简单了,由于call和apply就能够完成这个任务
(function() { // arguments就借用了数组的push方法 let result = Array.prototype.slice.call(arguments); console.log(result); // [1, 2, 3, 'hi'] })(1, 2, 3, 'hi'); Math.max.apply(null, [1,5,10]); // 数组借用了Math.max方法 复制代码
从以上代码中看出来了,你们都是相亲相爱的一家人。利用call和apply改变了this指向,方法中用到的this不再局限在原来指定的对象上了,加以泛化后获得更广的适用性
反柯里化的话题是由咱们亲爱的js之父发表的,咱们来从实际例子中去看一下它的做用
let slice = Array.prototype.slice.uncurrying(); (function() { let result = slice(arguments); // 这里只须要调用slice函数便可 console.log(result); // [1, 2, 3] })(1,2,3); 复制代码
以上代码经过反柯里化的方式,把Array.prototype.slice变成了一个通用的slice函数,这样就不会局限于仅对数组进行操做了,也从而将函数调用显得更为简洁清晰了
最后再来看一下它的实现方式吧,看代码,更逼真
Function.prototype.uncurrying = function() { let self = this; // self 此时就是下面的Array.prototype.push方法 return function() { let obj = Array.prototype.shift.call(arguments); /* obj实际上是这种样子的 obj = { 'length': 1, '0': 1 } */ return self.apply(obj, arguments); // 至关于Array.prototype.push(obj, 110) } }; let slice = Array.prototype.push.uncurrying(); let obj = { 'length': 1, '0': 1 }; push(obj, 110); console.log(obj); // { '0': 1, '1': 110, length: 2 } 复制代码
其实实现反柯里化的方式不仅一种,下面再给你们分享一种,直接看代码
Function.prototype.uncurrying = function() { let self = this; return function() { return Function.prototype.call.apply(self, arguments); } }; 复制代码
实现方式大体相同,你们也能够写一下试试,动动手,活动一下筋骨
下面再说一下函数节流,咱们都知道在onresize、onscroll和mousemove,上传文件这样的场景下,函数会被频繁的触发,这样很消耗性能,浏览器也会吃不消的
因而你们开始研究一种高级的方法,那就是控制函数被触发的频率,也就是函数节流了。简单说一下原理,利用setTimeout在必定的时间内,函数只触发一次,这样大大下降了频率问题
函数节流的实现也多种多样,这里咱们实现你们经常使用的吧
function throttle (fn, wait) { let _fn = fn, // 保存须要被延迟的函数引用 timer, flags = true; // 是否首次调用 return function() { let args = arguments, self = this; if (flags) { // 若是是第一次调用不用延迟,直接执行便可 _fn.apply(self, args); flags = false; return flags; } // 若是定时器还在,说明上一次还没执行完,不往下执行 if (timer) return false; timer = setTimeout(function() { // 延迟执行 clearTimeout(timer); // 清空上次的定时器 timer = null; // 销毁变量 _fn.apply(self, args); }, wait); } } window.onscroll = throttle(function() { console.log('滚动'); }, 500); 复制代码
给页面上body设置一个高度出现滚动条后试试看,比每滚动一下就触发来讲,大大下降了性能的损耗,这就是函数节流的做用,起到了事半功倍的效果,开发中也比较经常使用的
咱们知道有一个典故叫作:罗马不是一天建成的;更为通俗的来讲,胖纸也不是一天吃成的
体如今程序里也是同样,咱们若是一次得到了不少数据(好比有10W数据),而后在前端渲染的时候会卡到爆,浏览器那么温柔的物种都会起来骂娘了
因此在处理这么多数据的时候,咱们能够选择分批进行,不用一次塞辣么多,嘴就辣么大
下面来看一下简单的实现
function timeChunk(data, fn, count = 1, wait) { let obj, timer; function start() { let len = Math.min(count, data.length); for (let i = 0; i < len; i++) { val = data.shift(); // 每次取出一个数据,传给fn当作值来用 fn(val); } } return function() { timer = setInterval(function() { if (data.length === 0) { // 若是数据为空了,就清空定时器 return clearInterval(timer); } start(); }, wait); // 分批执行的时间间隔 } } // 测试用例 let arr = []; for (let i = 0; i < 100000; i++) { // 这里跑了10万数据 arr.push(i); } let render = timeChunk(arr, function(n) { // n为data.shift()取到的数据 let div = document.createElement('div'); div.innerHTML = n; document.body.appendChild(div); }, 8, 20); render(); 复制代码
兼容现代浏览器以及IE浏览器的事件添加方法就是一个很好的栗子
// 常规的是这样写的 let addEvent = function(ele, type, fn) { if (window.addEventListener) { return ele.addEventListener(type, fn, false); } else if (window.attachEvent) { return ele.attachEvent('on' + type, function() { fn.call(ele); }); } }; 复制代码
这样实现有一个缺点,就是在调用addEvent的时候都会执行分支条件里,其实只须要判断一次就好了,非要每次执行都来一波
下面咱们再来优化一下addEvent,以规避上面的缺点,就是咱们要实现的惰性加载函数了
let addEvent = function(ele, type, fn) { if (window.addEventListener) { addEvent = function(ele, type, fn) { ele.addEventListener(type, fn, false); } } else if (window.attachEvent) { addEvent = function(ele, type, fn) { ele.attachEvent('on' + type, function() { fn.call(ele) }); } } addEvent(ele, type, fn); }; 复制代码
上面的addEvent函数仍是个普通函数,仍是有分支判断。不过当第一次进入分支条件后,在内部就会重写了addEvent函数
下次再进入addEvent函数的时候,函数里就不存在条件判断了
节目不早,时间恰好,又到了该要说再见的时候了,来一个结束语吧
我勒个去,竟然罗列了这么多东西,你们看的也很辛苦了,早睡早起,好好休息吧!
这两篇也是为了给观察者模式起个头,以后会继续写文章来和你们好好分享的,谢谢各位小主,阿哥的观看了!哈哈