不废话了,我们先看看哪些是高阶函数
高阶函数 函数作为参数传递
让我们看一个简单的演示:
说实话,只是一个简单的,但是越写越激动,就成了一个小demo了。 也可以抄下来加油加醋(各种版本写的),玩得开心,PS:因为代码太多,占了文章的位置,把css样式去掉,实现自己喜欢的样式。
<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部分
// 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');
});
};
根据前面的代码,会得到如下的效果,我们来看看
不过还是感谢网易云音乐提供的API接口,让我们聆听美妙的音乐
函数输出作为返回值
各位朋友,函数作为返回值输出的应用场景太多了,这也凸显了函数式编程的思想。其实我们从闭包的例子中就已经听说过高阶函数了,哈哈
还记得我们判断数据类型的时候都是通过Object.prototype.toString来估计的。只有'[object XXX]'在各个数据类型之间是不同的
所以我们在写类型判断的时候,通常都会将参数传入到函数中。 这里我简单写一下实现。 我们先来看看。
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 次才能输出 'full',相当形象。
这种预设的功能也是巧妙的装饰器模式在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();
这个高级函数的栗子可以说是一石二鸟。 它不仅将函数作为参数传递,还将函数作为返回值输出。
单例模式也是一种非常实用的设计模式,后面的文章会对其进行分析,敬请期待,哈哈,我们来看看高阶函数还有哪些用途
柯里化其他应用函数
柯里化也称为部分求值。 柯里化函数会接收一些参数,然后不会立即求值,而是继续返回一个新函数,传入的参数会保存在闭包中,直到真正求值为止。 value,一次性评估所有传入参数
还能简单点吗?在一个函数中填入几个参数,然后返回一个新函数,最后求值,就没有了,是不是很简单?
再简单,也不如几行代码演示那么清晰
// 普通函数
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变成了通用的切片函数,这样就不会局限于只能对链表进行操作,从而使函数调用更加简洁明了
最后我们看一下它的实现方法,看代码,比较现实
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时会执行分支条件。 其实只需要判断一次jquery判断滚动条,每次都要执行一波。
接下来我们再优化一下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函数时,函数中就不会再进行条件判断了
结尾
演出时间不早,时间刚刚好,又到了告别的时候了,让我做个总结吧
高阶函数
反柯里化
功能节流
分时功能
延迟加载
卧槽,我罗列了这么多,大家辛苦看完了,早睡早起,好好休息!
读三件事
如果你觉得这篇内容对你很有启发,我想邀请你帮我三个小忙:
点击“在看”jquery判断滚动条,让更多人看到此内容(不管你喜不喜欢,都是流氓行为-_-)
发表评论