High-order function 中文译作“高阶函数”。不论是英文名还是中文名,都不直观,单纯通过名字无法想象它是个什么东西。其实它就是 “把function作为参数,或者返回值为function的函数”。
函数,本质上来讲,是把一组行为抽象成一个概念。这种抽象有两个好处:
一、便于理解代码的意图(前提是有一个合适的函数名)。
二、便于复用。
既然函数是一个概念,或者说,是一个抽象、一个变量,那么,把它作为其他函数的参数也就没什么奇怪的了。
高阶函数、函数式编程、lamda表达式等,其实都是一码事。而且,已经成了开发语言的标配。swift、kotlin这些新秀就不必说了,比较“现代”的开发语言对此都有完整的支持。
当年用C语言时,偶尔也会定义函数类型的变量,一直记不清楚需要写几个括号。
1. Abstraction
例子,对1-10 求和。
方法一:
var total = 0, count = 1;
while (count <= 10) {
total += count;
count += 1;
}
console.log(total);
方法二:
console.log(sum(range(1, 10)));
方法二的写法更清晰,不过,实际上用到的代码更多,因为sum和range这两个函数的代码要超过“方法一”的代码行数。
虽然方法二的代码会多一些,但我们更倾向于用这种方法。 因为,它抽象出来两个概念:range 和 sum,让代码更清晰,更不容易出Bug。
2. Abstracting Array Traversal
例子,逐个打印数组中的元素:
var array = [1, 2, 3];
for (var i = 0; i < array.length; i++) {
var current = array[i];
console.log(current);
}
我们可以抽象出来一个概念:logEach
function logEach(array) {
for (var i=0; i<array.length; i++) {
console.log(array[i]);
}
}
这样,以后再遍历打印别的数组时,就可以用这个函数了,不用每次写那么些代码。
但,这个logEach的通用性比较差,只能log,通过增加一个参数action,我们可以让它更通用一些。
function forEach(array, action) {
for (var i = 0; i < array.length; i++) {
action(array[i]);
}
}
forEach(["Wampeter", "Foma", "Granfalloon"], console.log);
这个例子还是在逐个打印数组元素。
看一个不一样的:求和。
var numbers = [1, 2, 3, 4, 5], sum = 0;
forEach(numbers, function(number) {
sum += number;
});
console.log(sum);
等等,跨度有点大,function(number) 这个number是哪来的?
forEach 的第二个参数是个action,也就是这个function:
function(number) {
sum += number;
}
在forEach内部,给action函数传递的参数是 array[i] ,所以,这个number 就是数组的一个元素。
3. Higher-Order Functions
把function作为参数,或者返回值为function的函数,叫做higher-order function。
例1:
function greaterThan(n) {
return function(m) { return m > n; };
}
var greaterThan10 = greaterThan(10);
console.log(greaterThan10(11));
例2:
function noisy(f) {
return function(arg) {
console.log("calling with", arg);
var val = f(arg);
console.log("called with", arg, "- got", val);
return val;
};
}
noisy(Boolean)(0);
// → calling with 0
// → called with 0 - got false
例3:
function unless(test, then) {
if (!test) then();
}
function repeat(times, body) {
for (var i = 0; i < times; i++) body(i);
}
repeat(3, function(n) {
unless(n % 2, function() {
console.log(n, "is even");
});
});
// → 0 is even
// → 2 is even
4. JSON
JavaScript Object Notation
JSON.stringify( )
JSON.parse( )
本章源码中有个ancestry.js, 包含这个文件之后,可以用 JSON.parse(ANCESTRY_FILE) 得到一个家族树中所有人员的数组。
var ancestry = JSON.parse(ANCESTRY_FILE);
后面代码中遇到的ancestry 变量都是这个。
5. Filtering an Array
用test函数来过滤一个数组,所有通过test的元素会构建一个新的数组。
function filter(array, test) {
var passed = [];
for (var i = 0; i < array.length; i++) {
if (test(array[i]))
passed.push(array[i]);
}
return passed;
}
ancestry.filter(function(person){
return person.father == "Carel Haverbeke";
});
6. Transforming with Map
用transform函数,改变数组元素的类型。
function map(array, transform) {
var mapped = [];
for (var i = 0; i < array.length; i++) {
mapped.push(transform(array[i]));
}
return mapped;
}
ancestry.map(function(person) {
return person.name;
});
7. Summarizing with Reduce
function reduce(array, combine, start) {
var current = start;
for (var i = 0; i < array.length; i++) {
current = combine(current, array[i]);
}
return current;
}
[1,2,3,4].reduce(function(a,b) {
return a+b;
});
ancestry.reduce(function(min, cur) {
if(cur.born < min.born)
return cur;
else
return min;
});
JavaScript 标准库中的reduce方法,提供了一个便利的功能,那就是,可以省略 start 参数。 默认把数组的第一个参数作为start。
8. Composability
function average(array) {
function plus(a, b) { return a + b; }
return array.reduce(plus) / array.length;
}
function age(p) { return p.died - p.born; }
function male(p) { return p.sex == "m"; }
function female(p) { return p.sex == "f"; }
var maleAverageAge = average(ancestry.filter(male).map(age));
var femaleAverageAge = average(ancestry.filter(female).map(age));
很神奇啊,写了多年的代码,已经习惯了从问题域出发,按照人类思维一行一行的顺序写代码。 要做到这种抽象层次,还要做很多练习才行。
9. The Cost
相比于直接使用循环,上面的方法比较低效,因为,它们增加了很多的函数调用,函数调用本身会增加内存和CPU的负载。
但,现在的计算机都非常快,这些性能上的损耗可以忽略不计。
10. Binding
每个function 都有一个bind方法。 这句话有点奇怪哈。 可以认为 function 是一种对象,它也有一些内置的成员函数,其中一个成员函数叫bind。
bind会生成一个函数。本质上来讲,bind是另外一种使用函数的方法,这一章讲的不清楚,没看懂,下一章还会仔细讲。先看个例子:
var theSet = ["Carel Haverbeke", "Maria van Brussel",
"Donald Duck"];
function isInSet(set, person) {
return set.indexOf(person.name) > -1;
}
console.log(ancestry.filter(function(person) {
return isInSet(theSet, person);
}));
// → [{name: "Maria van Brussel", …},
// {name: "Carel Haverbeke", …}]
console.log(ancestry.filter(isInSet.bind(null, theSet)));
// → … same result
11. 练习一、Flattening
使用reduce,把一个多维数组转换成一维数组。例如:
var arrays = [[1, 2, 3], [4, 5], [6]];
// → [1, 2, 3, 4, 5, 6]
arrays.reduce(function(a,b){
return a.concat(b);
});
12. 练习二、计算母子年龄差的平均值
用reduce计算平均值:
function average(array) {
function plus(a, b) { return a + b; }
return array.reduce(plus) / array.length;
}
用forEach生成一个 name - person 映射,便于查找母亲:
var byName = {};
ancestry.forEach(function(person) {
byName[person.name] = person;
});
计算一个人的母子年龄差:
function diff(person) {
var mother = byName[person.mother];
if (mother) {
return person.born - mother.born;
}
else
return null;
}
用map 把person转换成母子年龄差,用filter过滤掉找不到母亲的人,然后计算平均值:
var diff = average(ancestry.map(diff).filter(function (diffAge) {
return diffAge != null;
}));
console.log(diff);
13. 练习三、计算家族树中每个世纪的平均寿命
var centuryGroup = [];
ancestry.forEach(function (person) {
var century = Math.ceil(person.died/100);
var age = person.died-person.born;
if (century in centuryGroup) {
centuryGroup[century].push(age);
}
else {
centuryGroup[century] = [age];
}
});
for(var century in centuryGroup) {
console.log(century + ': ' + average(centuryGroup[century]));
}
14. 练习四、every and some
针对array,JavaScript标准库中提供了两个函数every 和 some。
例如:
[2,3,NaN].some(isNaN);
[2,3,4].every(isNaN);
自己写代码实现这两个函数,但不要作为array的方法,而是把array作为它们的参数。
function some(array, action) {
var ret = false;
for (var i=0; i<array.length; i++) {
if (action(array[i])) {
ret = true;
break;
}
}
return ret;
}
function every(array, action) {
var ret = true;
for (var i=0; i<array.length; i++) {
if (!action(array[i])) {
ret = false;
break;
}
}
return ret;
}