JavaScript的函数其实都指向某个变量。既然变量可以指向函数,函数的参数能接收变量,那么一个函数就可以接收另一个函数作为参数,这种函数就称之为高阶函数
一个最简单的高阶函数:
'use strict'; function add(x, y, f) { return f(x) + f(y); } x = -5; y = 6; f = Math.abs; f(x) + f(y) => Math.abs(-5) + Math.abs(6) => 11; return 11;
代码实现
'use strict'; function add(x, y, f) { return f(x) + f(y); } var x = add(-5, 6, Math.abs); // 11 console.log(x); >>> 11
由于map()
方法定义在JavaScript的Array
中,我们调用Array
的map()
方法,传入我们自己的函数,就得到了一个新的Array
作为结果
'use strict'; function pow(x) { return x * x; } var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9]; var results = arr.map(pow); // [1, 4, 9, 16, 25, 36, 49, 64, 81] console.log(results); >>> 1,4,9,16,25,36,49,64,81
map()作为高阶函数,事实上它把运算规则抽象了,因此,我们不但可以计算简单的f(x)=x2,还可以计算任意复杂的函数,比如,把
Array`的所有数字转为字符串
var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9]; arr.map(String); // ['1', '2', '3', '4', '5', '6', '7', '8', '9']
请把用户输入的不规范的英文名字,变为首字母大写,其他小写的规范名字。输入:['adam', 'LISA', 'barT']
,输出:['Adam', 'Lisa', 'Bart']
'use strict'; function normalize(arr) { return arr.map(function (name) { return name.charAt(0).toUpperCase() + name.slice(1).toLowerCase(); }) } if (normalize(['adam', 'LISA', 'barT']).toString() === ['Adam', 'Lisa', 'Bart'].toString()) { console.log('测试通过!'); } else { console.log('测试失败!'); }
字符串变成整数
'use strict'; var arr = ['1', '2', '3']; var r; r = arr.map(function (x) { return parseInt(x) }); console.log(r); >>> 1,2,3
Array的reduce()
把一个函数作用在这个Array
的[x1, x2, x3...]
上,这个函数必须接收两个参数,reduce()
把结果继续和序列的下一个元素做累积计算,其效果就是
[x1, x2, x3, x4].reduce(f) = f(f(f(x1, x2), x3), x4)
Array求和,就可以用
reduce`实现
var arr = [1, 3, 5, 7, 9]; arr.reduce(function (x, y) { return x + y; }); // 25
要把[1, 3, 5, 7, 9]
变换成整数13579,reduce()
也能派上用场
var arr = [1, 3, 5, 7, 9]; arr.reduce(function (x, y) { return x * 10 + y }); >>> 13579
filter也是一个常用的操作,它用于把Array
的某些元素过滤掉,然后返回剩下的元素
filter()
把传入的函数依次作用于每个元素,然后根据返回值是true
还是false
决定保留还是丢弃该元素
在一个Array
中,删掉偶数,只保留奇数,可以这么写:
var arr = [1, 2, 4, 5, 6, 9, 10, 15]; var r = arr.filter(function (x) { return x % 2 !== 0; }); r; // [1, 5, 9, 15]
把一个Array
中的空字符串删掉
var arr = ['A', '', 'B', null, undefined, 'C', ' ']; var r = arr.filter(function (s) { return s && s.trim(); // 注意:IE9以下的版本没有trim()方法 }); r; // ['A', 'B', 'C']
去除Array
的重复元素
'use strict'; var r, arr = ['apple', 'strawberry', 'banana', 'pear', 'apple', 'orange', 'orange', 'strawberry']; r = arr.filter(function (element, index, self) { return self.indexOf(element) === index; }); console.log(r.toString()); >>> apple,strawberry,banana,pear,orange
去除重复元素依靠的是
indexOf
总是返回第一个元素的位置,后续的重复元素位置与indexOf
返回的位置不相等,因此被filter
滤掉了。
尝试用filter()
筛选出素数
'use strict'; function get_primes(arr) { return arr.filter(function (x) { if (x <= 1) { return false; } for (let i=2; i<x; i++) { if (x % i === 0) { return false; } } return true; }); }
filter()
接收的回调函数,其实可以有多个参数。通常我们仅使用第一个参数,表示Array
的某个元素。回调函数还可以接收另外两个参数,表示元素的位置和数组本身
var arr = ['A', 'B', 'C']; var r = arr.filter(function (element, index, self) { console.log(element); // 依次打印'A', 'B', 'C' console.log(index); // 依次打印0, 1, 2 console.log(self); // self就是变量arr return true; });
JavaScript的Array
的sort()
方法就是用于排序的,但是排序结果可能让你大吃一惊:
// 看上去正常的结果: ['Google', 'Apple', 'Microsoft'].sort(); // ['Apple', 'Google', 'Microsoft']; // apple排在了最后: ['Google', 'apple', 'Microsoft'].sort(); // ['Google', 'Microsoft", 'apple'] // 无法理解的结果: [10, 20, 1, 2].sort(); // [1, 10, 2, 20]
第二个排序把
apple
排在了最后,是因为字符串根据ASCII码进行排序,而小写字母a
的ASCII码在大写字母之后。第三个排序结果是什么鬼?简单的数字排序都能错?
这是因为
Array
的sort()
方法默认把所有元素先转换为String再排序,结果'10'
排在了'2'
的前面,因为字符'1'
比字符'2'
的ASCII码小。
要按数字大小排序,我们可以这么写
'use strict'; var arr = [10, 20, 1, 2]; arr.sort(function (x, y) { if (x < y) { return -1; } if (x > y) { return 1; } return 0; }) console.log(arr); >>>
如果要倒序排序,我们可以把大的数放前面
'use strict'; var arr = [10, 20, 1, 2]; arr.sort(function (x, y) { if (x < y) { return 1; } if (x > y) { return -1; } return 0; }); // [20, 10, 2, 1]
默认情况下,对字符串排序,是按照ASCII的大小比较的,现在,我们提出排序应该忽略大小写,按照字母序排序。要实现这个算法,不必对现有代码大加改动,只要我们能定义出忽略大小写的比较算法就可以:
var arr = ['Google', 'apple', 'Microsoft']; arr.sort(function (s1, s2) { x1 = s1.toUpperCase(); x2 = s2.toUpperCase(); if (x1 < x2) { return -1; } if (x1 > x2) { return 1; } return 0; }); // ['apple', 'Google', 'Microsoft']
sort()
方法会直接对Array
进行修改,它返回的结果仍是当前Array
:
var a1 = ['B', 'A', 'C']; var a2 = a1.sort(); a1; // ['A', 'B', 'C'] a2; // ['A', 'B', 'C'] a1 === a2; // true, a1和a2是同一对象
高阶函数除了可以接受函数作为参数外,还可以把函数作为结果值返回
对Array
的求和
function sum(arr) { return arr.reduce(function (x, y) { return x + y; }); } sum([1, 2, 3, 4, 5]); // 15
如果不需要立刻求和,而是在后面的代码中,根据需要再计算怎么办?可以不返回求和的结果,而是返回求和的函数!
function lazy_sum(arr) { var sum = function () { return arr.reduce(function (x, y) { return x + y; }); } return sum; } var f = lazy_sum([1, 2, 3, 4, 5]); console.log(f()) >>> 15
当
lazy_sum
返回函数sum
时,相关参数和变量都保存在返回的函数中,这种称为“闭包(Closure)”的程序结构拥有极大的威力
另一个需要注意的问题是,返回的函数并没有立刻执行,而是直到调用了f()
才执行
function count() { var arr = []; for (var i=1; i<=3; i++) { arr.push(function () { return i * i; }); } return arr; } var results = count(); var f1 = results[0]; var f2 = results[1]; var f3 = results[2]; console.log(f1()) console.log(f2()) console.log(f3()) >>> 16 16 16
全部都是
16
!原因就在于返回的函数引用了变量i
,但它并非立刻执行。等到3个函数都返回时,它们所引用的变量i
已经变成了4
,因此最终结果为16
返回函数不要引用任何循环变量,或者后续会发生变化的变量
如果一定要引用循环变量怎么办?方法是再创建一个函数,用该函数的参数绑定循环变量当前的值,无论该循环变量后续如何更改,已绑定到函数参数的值不变:
function count() { var arr = []; for (var i=1; i<=3; i++) { arr.push((function (n) { return function () { return n * n; } })(i)); } return arr; } var results = count(); var f1 = results[0]; var f2 = results[1]; var f3 = results[2]; f1(); // 1 f2(); // 4 f3(); // 9
在没有class
机制,只有函数的语言里,借助闭包,同样可以封装一个私有变量。我们用JavaScript创建一个计数器:
'use strict'; function create_counter(initial) { var x = initial || 0; return { inc: function () { x += 1; return x; } } }
它用起来像这样:
var c1 = create_counter(); c1.inc(); // 1 c1.inc(); // 2 c1.inc(); // 3 var c2 = create_counter(10); c2.inc(); // 11 c2.inc(); // 12 c2.inc(); // 13
在返回的对象中,实现了一个闭包,该闭包携带了局部变量
x
,并且,从外部代码根本无法访问到变量x
。换句话说,闭包就是携带状态的函数,并且它的状态可以完全对外隐藏起来。
闭包还可以把多参数的函数变成单参数的函数。例如,要计算xy可以用Math.pow(x, y)
函数,不过考虑到经常计算x2或x3,我们可以利用闭包创建新的函数pow2
和pow3
:
'use strict'; function make_pow(n) { return function (x) { return Math.pow(x, n); } } var pow2 = make_pow(2); var pow3 = make_pow(3); console.log(pow2(5)); console.log(pow3(7)); >>> 25 343
x => x * x
上面的箭头函数相当于:
function (x) { return x * x; }
箭头函数相当于匿名函数,并且简化了函数定义。箭头函数有两种格式,一种像上面的,只包含一个表达式,连{ ... }
和return
都省略掉了。还有一种可以包含多条语句,这时候就不能省略{ ... }
和return
:
x => { if (x > 0) { return x * x; } else { return - x * x; } }
如果参数不是一个,就需要用括号()
括起来:
// 两个参数: (x, y) => x * x + y * y // 无参数: () => 3.14 // 可变参数: (x, y, ...rest) => { var i, sum = x + y; for (i=0; i<rest.length; i++) { sum += rest[i]; } return sum; }
如果要返回一个对象,就要注意,如果是单表达式,这么写的话会报错:
// SyntaxError: x => { foo: x }
因为和函数体的{ ... }
有语法冲突,所以要改为:
// ok: x => ({ foo: x })
箭头函数看上去是匿名函数的一种简写,但实际上,箭头函数和匿名函数有个明显的区别:箭头函数内部的this
是词法作用域,由上下文确定
由于JavaScript函数对this
绑定的错误处理,下面的例子无法得到预期结果:
var obj = { birth: 1990, getAge: function () { var b = this.birth; // 1990 var fn = function () { return new Date().getFullYear() - this.birth; // this指向window或undefined }; return fn(); } }; console.log(obj.getAge()); >>> NaN
现在,箭头函数完全修复了this
的指向,this
总是指向词法作用域,也就是外层调用者obj
:
var obj = { birth: 1990, getAge: function () { var b = this.birth; // 1990 var fn = () => new Date().getFullYear() - this.birth; // this指向obj对象 return fn(); } }; obj.getAge(); // 28
由于this
在箭头函数中已经按照词法作用域绑定了,所以,用call()
或者apply()
调用箭头函数时,无法对this
进行绑定,即传入的第一个参数被忽略:
var obj = { birth: 1990, getAge: function (year) { var b = this.birth; // 1990 var fn = (y) => y - this.birth; // this.birth仍是1990 return fn.call({birth:2000}, year); } }; obj.getAge(2015); // 28
一个generator看上去像一个函数,但可以返回多次
generator和函数不同的是,generator由function*
定义(注意多出的*
号),并且,除了return
语句,还可以用yield
返回多次
斐波那契数列的函数,可以这么写:
function fib(max) { var t, a = 0, b = 1, arr = [0, 1]; while (arr.length < max) { [a, b] = [b, a + b]; arr.push(b); } return arr; } // 测试: fib(5); // [0, 1, 1, 2, 3] fib(10); // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
如果换成generator,就可以一次返回一个数,不断返回多次。用generator改写如下:
function* fib(max) { var t, a = 0, b = 1, n = 0; while (n < max) { yield a; [a, b] = [b, a + b]; n ++; } return; }
调用generator对象有两个方法,一是不断地调用generator对象的next()
方法:
var f = fib(5); f.next(); // {value: 0, done: false} f.next(); // {value: 1, done: false} f.next(); // {value: 1, done: false} f.next(); // {value: 2, done: false} f.next(); // {value: 3, done: false} f.next(); // {value: undefined, done: true}
用for ... of
循环迭代generator对象,这种方式不需要我们自己判断done
:
'use strict' function* fib(max) { var t, a = 0, b = 1, n = 0; while (n < max) { yield a; [a, b] = [b, a + b]; n ++; } return; } for (var x of fib(10)) { console.log(x); // 依次输出0, 1, 1, 2, 3, ... } >>> 0 1 1 2 3 5 8 13 21 34
因为generator可以在执行过程中多次返回,所以它看上去就像一个可以记住执行状态的函数,利用这一点,写一个generator就可以实现需要用面向对象才能实现的功能。例如,用一个对象来保存状态,得这么写:
var fib = { a: 0, b: 1, n: 0, max: 5, next: function () { var r = this.a, t = this.a + this.b; this.a = this.b; this.b = t; if (this.n < this.max) { this.n ++; return r; } else { return undefined; } } };
generator还有另一个巨大的好处,就是把异步回调代码变成“同步”代码。这个好处要等到后面学了AJAX以后才能体会到
没有generator之前的黑暗时代,用AJAX时需要这么写代码:
ajax('http://url-1', data1, function (err, result) { if (err) { return handle(err); } ajax('http://url-2', data2, function (err, result) { if (err) { return handle(err); } ajax('http://url-3', data3, function (err, result) { if (err) { return handle(err); } return success(result); }); }); });
回调越多,代码越难看。
有了generator的美好时代,用AJAX时可以这么写:
try { r1 = yield ajax('http://url-1', data1); r2 = yield ajax('http://url-2', data2); r3 = yield ajax('http://url-3', data3); success(r3); } catch (err) { handle(err); }
注意这里用了一个“创建一个匿名函数并立刻执行”的语法:
(function (x) { return x * x; })(3); // 9
创建一个匿名函数并立刻执行可以这么写:
function (x) { return x * x } (3);
由于JavaScript语法解析的问题,会报SyntaxError错误,因此需要用括号把整个函数定义括起来:
(function (x) { return x * x }) (3);
一个立即执行的匿名函数可以把函数体拆开,一般这么写:
(function (x) { return x * x; })(3);