一、函数柯里化
currying 又称为部分求值。一个 currying 的函数首先会接受一些参数,接受了这些参数之后,该函数并不会立即求值,而是继续返回另外一个函数,刚才传入的参数在函数形成的闭包中被保存起来。待到函数被真正需要求值的时候,之前传入的所有参数都会被一次性用于求值。下面来看一个例子。
假设我们要编写一个计算每月开销的函数。在每天结束之前,我们都要记录今天花掉了多少钱。代码如下:
var monthlyCost = 0;
var cost = function(money){
monthlyCost += money;
};
cost(100); // 第1天开销
cost(200); // 第2天开销
cost(300); // 第3天开销
// cost(800); // 第30天开销
console.log(monthlyCost) // 输出:600
通过这段代码可以看到,每天结束后我们都会记录并计算到今天为止花掉的钱。但我们其实并不太关心每天花掉了多少钱,而只想知道到月底的时候会花掉多少钱。也就是说,实际上只需要在月底计算一次。
如果在每个月的前29天(假设30天为一个月),我们都只是保存好当天的开销,直到第30天才进行求值计算,这样就达到了我们的要求。虽然下面的 cost 函数还不是一个 currying 函数的完整实现,但有助于我们了解其思想:
var cost = (function () {
var args = [];
return function () {
if (arguments.length == 0) {
var money = 0;
for (var i = 0; i < args.length; i++) {
money += args[i]
};
return money;
} else {
[].push.apply(args, arguments);
}
}
})();
cost(100); // 未真正求值
cost(200); // 未真正求值
cost(300); // 未真正求值
console.log(cost()); // 求值并输出600
接下来我们编写一个通用的 function currying(){}, 接受一个参数,即将要被 currying 的函数。在这个例子里,这个函数的作用遍历本月每天的开销并求出他们的总和。代码如下:
var currying = function (fn) {
var args = [];
return function () {
if (arguments.length === 0) {
return fn.apply(this, args);
};
[].push.apply(args, arguments);
return arguments.callee;
}
};
var cost = (function () {
var money = 0;
return function () {
for (var i = 0; i < arguments.length; i++) {
money += arguments[i]
};
return money;
}
})();
var cost = currying(cost); // 转化成 currying 函数
cost(100); // 未真正求值
cost(200); // 未真正求值
cost(300); // 未真正求值
console.log(cost()); // 求值并输出600
至此,我们完成了一个 currying 函数的编写。当调用 cost() 时,如果明确地带上了一些参数,表示此时并不进行真正的求值计算,而是把这些参数保存起来,此时让 cost 函数返回另外一个函数。只有当我们以不带参数的形式执行 cost() 时,才利用前面保存的所有参数,真正开始进行求值计算。
二、函数节流
JavaScript 中的函数大多数情况下都是由用户主动触发调用的。除非是函数本身的实现不合理,否则我们一般不会遇到跟函数性能相关的问题。但在一些少数情况下,函数的触发不是由用户直接控制的。在这些场景下,函数有可能被非常频繁地调用,而造成大的性能问题。下面看一个例子。
比如我们在 window.onresize 事件中要执行一些操作,在我们通过拖拽来改变窗口大小的时候,执行的操作在1秒钟进行了10次,而我们实际上只需要2次或3次,这就需要我们按时间段来忽略掉一些事件操作请求,比如确保在500ms内只执行一次。关于函数节流的代码实现有许多种,下面我们借助 setTimeout 来完成这件事情,代码如下:
var throttle = function (fn, delay) {
var timer, // 定时器
firstFlag = true; // 是否为第一次调用
return function () {
var ctx = this, args = arguments;
if (firstFlag) { // 如果是第一次调用,不需延迟执行
fn && fn.apply(ctx, args);
return firstFlag = false;
}
if (timer) { // 如果定时器还在,说明前一次延迟执行还没有完成
return false;
}
timer = setTimeout(function () { // 设置延迟执行
clearTimeout(timer);
timer = null;
fn && fn.apply(ctx, args);
}, delay || 500)
};
};
window.onresize = throttle(function (e) {
console.log(e)
}, 1000);
可以看到,上面的 throttle 函数的原理是,将即将被执行的函数用 setTimeout 延迟一段时间执行,如果该次延迟执行还没有完成,则忽略接下来调用该函数的请求。throttle 函数接受2个参数,第一个参数为需要被延迟执行的函数,第二个参数为延迟执行的时间。
三、函数防抖
除了节流函数,防抖也是应用场景比较广泛的函数,比如实时搜索。基于性能考虑,肯定不能用户每输入一个字符就发送一次搜索请求,一种方法就是等待用户停止输入,比如等待了500ms用户没有再输入,那么就发送搜索请求。下面代码也是借助 setTimeout 来完成:
<input id="ipt" placeholder="防抖函数输入">
<script>
var debounce = function (fn, wait) {
var timer = null;
return function () {
var ctx = this, args = arguments;
if (timer) clearInterval(timer); // 如果存在延迟执行函数,则清除
timer = setTimeout(function () {
fn && fn.apply(ctx, args)
}, wait || 300);
}
};
document.getElementById('ipt').oninput = debounce(function () {
console.log(this.value)
}, 500)
</script>
可以看到防抖函数的思路,在规定时间内未触发第二次,则执行。debounce 函数接受2个参数,第一个参数为需要被延迟执行的函数,第二个参数为等待执行的时间。
四、惰性加载函数
在 Web 开发中,因为浏览器之间的实现差异,一些嗅探工作总是不可避免。比如我们需要一个在各个浏览器中能够通用的事件绑定函数 addEvent,常见的写法如下:
var addEvent = function (elem, type, handler) {
if (window.addEventListener) {
return elem.addEventListener(type, handler, false)
};
if (window.attachEvent) {
return elem.attachEvent('on' + type, handler)
};
};
这个函数的缺点是,当它每次被调用的时候都会执行里面的 if 条件分支,虽然执行这些 if 分支的开销不算大,但也许有一些方法可以让程序避免这些重复的执行过程。
第二种方案是这样,我们把嗅探浏览器的操作提前到代码加载的时候,在代码加载的时候就立刻进行一次判断,以便让 addEvent 返回一个包裹了正确逻辑的函数。代码如下:
var addEvent = (function () {
if (window.addEventListener) {
return function (elem, type, handler) {
elem.addEventListener(type, handler, false)
}
};
if (window.attachEvent) {
return function (elem, type, handler) {
elem.attachEvent('on' + type, handler)
}
};
})();
目前的 addEvent 函数依然有个缺点,也许我们从头到尾都没有使用过 addEvent 函数,这样看来,前一次的浏览器嗅探就是完全多余的操作,而且这也会稍稍延长页面 ready 的时间。
第三种方案即是我们将要讨论的惰性载入函数方案。此时 addEvent 依然被声明为一个普通函数,在函数里依然有一些分支判断。但是在第一次进入条件分支之后,在函数内部会重写这个函数,重写之后的函数就是我们期望的 addEvent 函数,在下一次进入 addEvent 函数的时候,addEvent 函数里不再存在条件分支语句:
<button id="btn">点我绑定事件</button>
<script>
var addEvent = function (elem, type, handler) {
if (window.addEventListener) {
addEvent = function (elem, type, handler) {
elem.addEventListener(type, handler, false)
}
} else if (window.attachEvent) {
addEvent = function (elem, type, handler) {
elem.attachEvent('on' + type, handler)
}
};
addEvent(elem, type, handler);
};
var btn = document.getElementById('btn');
addEvent(btn, "click", function () {
console.log(1)
});
// 第二次绑定事件的时候, addEvent 函数已被重写,已经是我们期望的函数,不再存在条件分支语句
addEvent(btn, "click", function () {
console.log(2)
});
</script>
五、分时函数
在前面关于函数节流的讨论中,我们提供了一种限制函数被频繁调用的解决方案。下面我们将遇到另外一个问题,某些函数确实是用户主动调用的,但因为一些客观原因,这些函数会严重影响页面性能,下面看一个例子。
假如我们要创建一个 WebQQ 的 QQ 好友列表。列表中通常会有成百上千个好友,如果一个好友用一个节点来表示,当我们在页面中渲染这个列表的时候,可能要一次性往页面中创建成百上千个节点,在短时间内往页面中大量添加 DOM 节点显然也会让浏览器吃不消,我们看到的结果往往就是浏览器的卡顿甚至假死,代码如下:
var ary = [];
for (var i = 1; i <= 1000; i++) {
ary.push(i); // 假设 ary 装载了1000个好友的数据
};
var renderFriendList = function (data) {
for (var i = 0; i < data.length; i++) {
var div = document.createElement('div');
div.innerHTML = i;
document.body.appendChild(div);
}
};
renderFriendList(ary);
这个问题的解决方案之一就是下面的 timeChunk 函数,timeChunk 函数让创建节点的工作分批进行,比如把1秒钟创建1000个节点,改为每隔200毫秒创建8个节点。timeChunk 函数接受3个参数,第一个参数是创建节点时需要用到的数据,第二个参数是封装了创建节点逻辑的函数,第三个参数表示每一批创建的节点数量。代码如下:
var timeChunk = function (ary, fn, count) {
var timer;
var start = function () {
for (var i = 0; i < Math.min(count || 1, ary.length); i++) {
fn && fn(ary.shift())
}
};
return function () {
timer = setInterval(function () {
if (ary.length === 0) { // 如果全部节点都已经被创建好
return clearInterval(timer);
}
start();
}, 200); // 分批执行的时间间隔,也可以用参数的形式传入
};
};
下面我们进行一个小测试,假设我们有1000个好友的数据,我们利用 timeChunk 函数,每一批只往页面中创建8个节点:
var ary = [];
for (var i = 1; i <= 1000; i++) {
ary.push(i);
};
var renderFriendList = timeChunk(ary, function (item) {
var div = document.createElement('div');
div.innerHTML = item;
document.body.appendChild(div);
}, 8);
renderFriendList();
这样就实现了我们的期望,每隔200ms创建8个节点,分批进行。
总结
以上内容有摘自《JavaScript 设计模式与开发实践》,内容有删改。在这里做个笔记,给大家做个分享,如有不足,也可指出,希望大家都有所收获。也可以关注作者,以后会不定期分享心得笔记,与君共勉。