高阶函数

高阶函数是指满足下列条件之一的函数:

函数可以作为参数进行传递
函数可以作为返回值进行输出
JavaScript 语言中的函数显然满足高阶函数的条件,在实际开发中,无论是将函数当做参数传递,还是让函数的执行结果返回给另外一个函数,这两种情形都有很多应用场景。

函数作为参数传递

把函数当做参数进行传递,这代表我们可以抽离出一部分容易变化的业务逻辑,把这部分业务逻辑放在函数参数中,这样一来就可以分离业务代码中变化和不变的部分。

  • 回调函数

ajax 异步请求的应用中,回调函数的使用非常频繁。当我们想在 ajax 请求返回之后做些事情,但又并不知道请求返回的确切时间时,最常见的方案是把 callback 函数当做参数传入发起的 ajax 请求的方法中,待请求完成之后执行 callback 函数:

1
2
3
4
5
6
7
var getUserInfo = function (userId, callback) {
$.ajax("http://webape.net/getUserInfo?" + userId, function (data) {
if (typeof callback === "function") {
callback(data);
}
});
};

再来看一个例子,假设有这样一个需求,需要创建 100div 元素,同时把他们隐藏起来,那么可以看到下面这种实现:

1
2
3
4
5
6
7
8
9
var appendDiv = function () {
for (var i = 0; i < 100; i++) {
var div = document.createElement("div");
div.innerHTML = i;
document.body.appendChild(div);
div.style.display = "none";
}
};
appendDiv();

div.style.display = 'none'的逻辑硬编码在 appendDiv 里显然是不合理的,appendDiv 未免有点个性化,成为了一个难以复用的函数,并不是每个人创建了节点之后就希望它们立刻被隐藏。于是我们把这段代码抽离出来,用回调函数的形式传入 appendDiv 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
var appendDiv = function (callback) {
for (var i = 0; i < 100; i++) {
var div = document.createElement("div");
div.innerHTML = i;
document.body.appendChild(div);
if (typeof callback === "function") {
callback(div);
}
}
};
appendDiv(function (node) {
node.style.display = "none";
});
  • Array.prototype.sort

Array.prototype.sort 接受一个函数作为参数,这个函数里面封装了数组元素的排序顺序。我们的目的是对数组进行排序,这是不变的部分;但用什么规则去排序这是可变的部分。

1
2
3
4
// 从小到大排序
[3, 9, 8, 5].sort(function (a, b) {
return a - b;
});

函数作为返回值输出

  • 判断数据的类型

判断一个数据是否是数组,在以往的实现当中,可以基于鸭子类型的概念来判断,比如这个数据有没有 length 属性,有没有 sort 方法等。但更好的方法是用 Object.prototype.toString 来计算。根据 Object.prototype.toString.call( [1, 2, 3] ) 总是返回 ’[object Array]’Object.prototype.toStrng.call( ‘str’ ) 也总是返回 ’[object Array]’ 得出,
它总是会返回一个类似结构的字符串。于是用循环语句来批量注册类型判断的函数:

1
2
3
4
5
6
7
8
9
10
11
12
var Type = {};
for (var i = 0, type; (type = ["String", "Array", "Number"][i++]); ) {
(function (type) {
Type["is" + type] = function (obj) {
return (
Object.prototype.toString.call(obj) === "[object " + type + "]"
);
};
})(type);
}
Type.isArray([]); // true
Type.isString(""); // true

下面是一个单例模式的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
var getSingle = function (fn) {
var ret;
return function () {
return ret || (ret = fn.apply(this, arguments));
};
};
// 来看看这个单例的效果
var getScript = getSingle(function () {
return document.createElement("script");
});
var script1 = getScript();
var script2 = getScript();
console.log(script1 === script2); // true

高阶函数实现 AOP

AOP(面向切面编程)的主要作用是把一些跟核心业务逻辑模块无关的功能抽离出来,这些跟业务逻辑无关的功能通常包括日志统计、安全控制、异常处理等。把这些功能抽离出来后,再通过“动态植入”的方式掺入业务逻辑模块中。这样做的好处是首先保证了业务逻辑模块的纯洁和高内聚性,其实是可以很方便的复用这些日志统计等功能模块。在 JavaScript 中,我们可以通过Function.prototype 来实现 AOP:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Function.prototype.before = function (beforeFn) {
var _self = this;
return function () {
beforeFn.apply(this, arguments);
return _self.apply(this, arguments);
};
};
Function.prototype.after = function (afterFn) {
var _self = this;
return function () {
var ret = _self.apply(this, arguments);
afterFn.apply(this, arguments);
return ret;
};
};
var func = function () {
console.log(2);
};
func = func
.before(function () {
console.log(1);
})
.after(function () {
console.log(3);
});
func(); // 换行打印:1 2 3

高阶函数的其他应用

  • currying

currying 的概念最早由俄国数学家 Moses Schoofinkel 发明,而后由著名的数理逻辑学家 Haskell Curry 将其丰富和发展,currying 由此得名。

currying 又称部分求值。这里我们讨论的是函数柯里化( function currying )。一个柯里化函数首先会接受一些参数,接受了这些参数会后,该函数并不会立即求值,而是继续返回另一个函数,刚才传入的参数在函数形成的闭包中被保存起来。待到函数被真正求值的时候,之前传入的所有参数都会被一次性用于求值。来看一个例子:
假设我们要编写一个计算每月开销的函数。在每天结束之前,我们都要记录几天花掉了多少钱。

1
2
3
4
5
6
7
8
9
10
11
var monthCost = 0;
var cost = function (money) {
monthCost += money;
};
cost(100);
第一天;
cost(200);
第二天;
cost(300);
第三天;
console.log(monthCost); // 600

通过这段代码,我们可以看到每天都花了多少钱,但是如果我们只想知道每个月的消费如何的话,那就没必要计算每天的花费了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var cost = (function () {
var args = [];
return function () {
if (arguments.length === 0) {
var money = 0;
for (var i = 0, l = args.length; i < l; i++) {
money += args[i];
}
return money;
} else {
[].push.apply(args, arguments);
}
};
})();
cost(100);
cost(200);
cost(300);
console.log(cost()); // 600

接下来编写一个通用的柯里化函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
var currying = function (fn) {
var args = [];
return function () {
if (arguments.length === 0) {
return fn.apply(this, args);
} else {
[].push.apply(args, arguments);
return arguments.callee;
}
};
};
var cost = (function () {
var money = 0;
return function () {
for (var i = 0, l = arguments.length; i < l; i++) {
money += arguments[i];
}
return money;
};
})();
var cost = currying(cost);
cost(100);
cost(200);
cost(300);
console.log(cost()); // 600
  • uncurrying

uncurrying 是反柯里化,大概意思是扩大函数的应用范围,将本来只有特定对象才能使用的方法,扩展到更多的对象。
比如我们常常让类数组对象去借用 Array.prototype 的方法:

1
2
3
4
(function () {
Array.prototype.push.call(arguments, 4);
console.log(arguments); // [1, 2, 3, 4]
})(1, 2, 3);

uncurrying第一种实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Function.prototype.uncurrying = function () {
var self = this;
return function () {
var obj = Array.prototype.shift.call(arguments);
return self.apply(obj, arguments);
};
};
for (var i = 0, fn, ary = ["push", "shift", "forEach"]; (fn = ary[i++]); ) {
Array[fn] = Array.prototype[fn].uncurrying();
}
var obj = {
length: 3,
0: 1,
1: 2,
2: 3,
};
Array.push(obj, 4);
console.log(obj.length); // 4
var first = Array.shift(obj);
console.log(first); // 1
console.log(obj); // {0:2, 1:3, 2:4, length: 3}
Array.forEach(obj, function (i, n) {
console.log(n); // 分别输出:0, 1, 2
});

uncurrying的第二种实现:

1
2
3
4
5
6
Function.prototype.uncurrying = function () {
var self = this;
return function () {
return Function.prototype.call.apply(self, arguments);
};
};
  • 函数节流

JavaScript 中,大部分的函数都是由用户主动调动触发的。但是也存在少数情况,这些情况下函数的触发并不是又用户直接控制的。这个时候函数就有可能被频繁地调用,而造成大的性能问题。以下几个场景函数将被频繁调用:给 window 绑定了onresize 事件的时候,如果存在 DOM 相关的操作,那这个时候是非常耗性能的,严重的时候浏览器可能会卡顿;mousemove 事件,如果给某个元素绑定了拖拽事件,那么该函数也会被频繁的触发;在比如上传一个文件的时候,可能需要频繁的通知进度信息等。

函数节流就是为了避免函数被频繁地调用而存在的一种解决方案,从而优化性能。通常是用 setTimeout 来实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var throttle = function (fn, interval) {
var _self = fn,
timer,
firstTime = true;
return function () {
var args = arguments,
_me = this;
if (firstTime) {
_self.apply(_me, args);
return (firstTime = false);
}
if (timer) {
return false;
}
timer = setTimeout(function () {
clearTimeout(timer);
timer = null;
_self.apply(_me, args);
}, interval || 500);
};
};
window.onresize = throttle(function () {
console.log(1);
}, 500);
  • 分时函数

上面我们介绍了一种解决函数被频繁调用的方法。但是有时候,用户确实有这种需求,比如需要在短时间内把 1000 个 qq 好友渲染到列表上,这个时候就可能会很卡。但是如果把 1000ms 创建 1000 个节点,改成每 200ms 创建 8 个节点。这个时候就避免这种问题。
分时函数接受 3 个参数:第一个是创建节点的时候需要用到的数据,第二个是封装了创建节点的函数,第三个是每一批创建的节点数量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var timeChunk = function (ary, fn, count) {
var obj, t, start;
start = function () {
for (var i = 0; i < Math.min(count || 1, ary.length); i++) {
var obj = ary.shift();
fn(obj);
}
};
return function () {
t = setInterval(function () {
if (ary.length === 0) {
// 如果全部节点都已经创建好
clearInterval(t);
}
start();
}, 200);
};
};

分时函数有了,现在我们来测试一下。假设有 1000 个好友,利用 timeChunk 函数,每批往页面上渲染 8 个:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var ary = [];
for (var i = 0; i <= 1000; i++) {
ary.push(i);
}
var renderFriendList = timeChunk(
ary,
function (n) {
var div = document.createElement("div");
div.innerHTML = n;
document.body.appendChild(div);
},
8
);
renderFriendList();
  • 惰性加载函数

web 开发的过程中,因为浏览器之间的实现差异,一些嗅探工作总是避免不了的。比如我们需要一个能在各个浏览器都能通用的事件绑定函数 addEvent,常见的写法如下:

1
2
3
4
5
6
7
8
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 判断,虽然开销不大,但是有办法能避免这种操作:

1
2
3
4
5
6
7
8
9
10
11
12
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 函数的时候会重写事件绑定函数,在下次进去的时候就会直接执行事件绑定了。

1
2
3
4
5
6
7
8
9
10
11
12
13
var addEvent = function (elem, type, handler) {
if (window.addEventListener) {
addEvent = function (elem, type, handler) {
elem.addEventListener(type, handler, false);
};
}
if (window.attachEvent) {
addEvent = function (elem, type, handler) {
elem.attachEvent("on" + type, handler);
};
}
addEvent(elem, type, handler);
};