Eloquent JavaScript 笔记 五: High-Order Functions

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;
}







评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值