map/reduce
map()
方法定义在JavaScript的Array
中,我们调用Array
的map()
方法,传入我们自己的函数,就得到了一个新的Array
作为结果:
function pow(x) { return x * x; } var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9]; arr.map(pow); // [1, 4, 9, 16, 25, 36, 49, 64, 81]
map()
传入的参数是pow
,即函数对象本身。
你可能会想,不需要map()
,写一个循环,也可以计算出结果,的确也可以,但是,从循环代码来看,我们无法一眼看明白“把f(x)作用在Array的每一个元素并把结果生成一个新的Array”。所以,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‘]
Reduce
再看reduce的用法。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
parseInt的语法是这样的
parseInt(string, radix) radix // 可选。表示要解析的数字的基数。该值介于 2 ~ 36 之间。 // 如果省略该参数或其值为 0,则数字将以 10 为基础来解析。如果它以 “0x” 或 “0X” 开头,将以 16 为基数。 // 如果该参数小于 2 或者大于 36,则 parseInt() 将返回 NaN。
map是这样的
r = arr.map(function(value, index){ cosole.log(‘value‘ + value + ‘ index‘ + index); }); // value1 index0 // value2 index1 // value3 index2
parseInt会接收第二个参数index, 所以实际是执行的是
parseInt(‘1‘,0); parseInt(‘2‘,1); parseInt(‘3‘,2);
下面是一个将字符串转换为int的通过map实现的方法
function string2Int(s) { var arr = s.split(‘‘).map(function(x){ // typeof(x) -> string return x * 1; }); }
filter
和map()
类似,Array
的filter()
也接收一个函数。和map()
不同的是,filter()
把传入的函数依次作用于每个元素,然后根据返回值是true
还是false
决定保留还是丢弃该元素。
把一个Array
中的空字符串删掉,可以这么写:
var arr = [‘A‘, ‘‘, ‘B‘, null, undefined, ‘C‘, ‘ ‘]; var r = arr.filter(function (s) { return s && s.trim(); // 注意:IE9以下的版本没有trim()方法 }); r; // [‘A‘, ‘B‘, ‘C‘]
排序算法
排序也是在程序中经常用到的算法。无论使用冒泡排序还是快速排序,排序的核心是比较两个元素的大小。
sort()
方法也是一个高阶函数,它还可以接收一个比较函数来实现自定义的排序。
要按数字大小排序,我们可以这么写:
var arr = [10, 20, 1, 2]; arr.sort(function (x, y) { if (x < y) { return -1; } if (x > y) { return 1; } return 0; }); // [1, 2, 10, 20]
默认情况下,对字符串排序,是按照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是同一对象
函数作为返回值
高阶函数除了可以接受函数作为参数外,还可以把函数作为结果值返回。
如果不需要立刻求和,而是在后面的代码中,根据需要再计算怎么办?可以不返回求和的结果,而是返回求和的函数!
function lazy_sum(arr) { var sum = function () { return arr.reduce(function (x, y) { return x + y; }); } return sum; }
当我们调用lazy_sum()
时,返回的并不是求和结果,而是求和函数:
var f = lazy_sum([1, 2, 3, 4, 5]); // function sum()
调用函数f
时,才真正计算求和的结果:
f(); // 15
当我们调用lazy_sum()
时,每次调用都会返回一个新的函数,即使传入相同的参数:
var f1 = lazy_sum([1, 2, 3, 4, 5]); var f2 = lazy_sum([1, 2, 3, 4, 5]); f1 === f2; // false
闭包
注意到返回的函数在其定义内部引用了局部变量arr
,所以,当一个函数返回了一个函数后,其内部的局部变量还被新函数引用,所以,闭包用起来简单,实现起来可不容易。
另一个需要注意的问题是,返回的函数并没有立刻执行,而是直到调用了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];
在上面的例子中,每次循环,都创建了一个新的函数,然后,把创建的3个函数都添加到一个Array
中返回了。
你可能认为调用f1()
,f2()
和f3()
结果应该是1
,4
,9
,但实际结果是:
f1(); // 16 f2(); // 16 f3(); // 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; }
注意这里用了一个“创建一个匿名函数并立刻执行”的语法:
(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);
说了这么多,难道闭包就是为了返回一个函数然后延迟执行吗?
当然不是!闭包有非常强大的功能。举个栗子:
在面向对象的程序设计语言里,比如Java和C++,要在对象内部封装一个私有变量,可以用private
修饰一个成员变量。
在没有class
机制,只有函数的语言里,借助闭包,同样可以封装一个私有变量。我们用JavaScript创建一个计数器:
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
:
function make_pow(n) { return function (x) { return Math.pow(x, n); } } // 创建两个新函数: var pow2 = make_pow(2); var pow3 = make_pow(3); pow2(5); // 25 pow3(7); // 343
脑洞大开
很久很久以前,有个叫阿隆佐·邱奇的帅哥,发现只需要用函数,就可以用计算机实现运算,而不需要0
、1
、2
、3
这些数字和+
、-
、*
、/
这些符号。
JavaScript支持函数,所以可以用JavaScript用函数来写这些计算。来试试:
// 定义数字0: var zero = function (f) { return function (x) { return x; } }; // 定义数字1: var one = function (f) { return function (x) { return f(x); } }; // 定义加法: function add(n, m) { return function (f) { return function (x) { return m(f)(n(f)(x)); } } }
箭头函数
为什么叫Arrow Function?因为它的定义用的就是一个箭头:
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 })
var fn = x => x * x; fn(3); // 9
this
箭头函数看上去是匿名函数的一种简写,但实际上,箭头函数和匿名函数有个明显的区别:箭头函数内部的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(); } };
现在,箭头函数完全修复了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(); // 25
如果使用箭头函数,以前的那种hack写法:
var that = this;
就不再需要了。
由于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); // 25