JavaScript学习记录day8-闭包、箭头函数和生成器
JavaScript学习记录day8-闭包、箭头函数和生成器
[TOC]
1. 函数作为返回值
高阶函数除了可以接受函数作为参数外,还可以把函数作为结果值返回。
我们来实现一个对Array
的求和。通常情况下,求和的函数是这样定义的:
1 2 3 4 5 6 7 8 |
function sum(arr) { return arr.reduce(function (x, y) { return x + y; }); } console.log(sum([1, 2, 3, 4, 5])); // 15 |
但是,如果不需要立刻求和,而是在后面的代码中,根据需要再计算怎么办?可以不返回求和的结果,而是返回求和的函数!
1 2 3 4 5 6 7 8 9 |
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
中又定义了函数sum
,并且,内部函数sum
可以引用外部函数lazy_sum
的参数和局部变量,当lazy_sum
返回函数sum
时,相关参数和变量都保存在返回的函数中,这种称为“闭包(Closure)”的程序结构拥有极大的威力。
请再注意一点,当我们调用lazy_sum()
时,每次调用都会返回一个新的函数,即使传入相同的参数:
1 2 3 4 5 6 7 |
var f1 = lazy_sum([1, 2, 3, 4, 5]); console.log(f1); // [Function: sum] var f2 = lazy_sum([1, 2, 3, 4, 5]); console.log(f1); // [Function: sum] console.log(f1 === f2); // false |
f1()
和f2()
的调用结果互不影响。
2. 闭包
注意到返回的函数在其定义内部引用了局部变量arr,所以,当一个函数返回了一个函数后,其内部的局部变量还被新函数引用,所以,闭包用起来简单,实现起来可不容易。
另一个需要注意的问题是,返回的函数并没有立刻执行,而是直到调用了f()
才执行。我们来看一个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
'use strict'; function icount() { var arr = []; for (var i=1; i<=3; i++) { arr.push(function () { return i * i; }); } return arr; } var results = icount(); var f1 = results[0]; var f2 = results[1]; var f3 = results[2]; |
在上面的例子中,每次循环,都创建了一个新的函数,然后,把创建的3个函数都添加到一个Array
中返回了。
你可能认为调用f1()
,f2()
和f3()
结果应该是1
,4
,9
,但实际结果是:
1 2 3 4 |
console.log(f1()); // 16 console.log(f2()); // 16 console.log(f3()); // 16 |
全部都是16
!原因就在于返回的函数引用了变量i
,但它并非立刻执行。等到3个函数都返回时,它们所引用的变量i
已经变成了4
,因此最终结果为16
。
返回闭包时牢记的一点就是:返回函数不要引用任何循环变量,或者后续会发生变化的变量。
如果一定要引用循环变量怎么办?方法是再创建一个函数,用该函数的参数绑定循环变量当前的值,无论该循环变量后续如何更改,已绑定到函数参数的值不变:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
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 |
注意这里用了一个“创建一个匿名函数并立刻执行”的语法:
1 2 3 4 |
(function (x) { return x * x; })(3); // 9 |
理论上讲,创建一个匿名函数并立刻执行可以这么写:
1 2 |
function (x) { return x * x } (3); |
但是由于JavaScript语法解析的问题,会报SyntaxError错误,因此需要用括号把整个函数定义括起来:
1 2 |
(function (x) { return x * x }) (3); |
通常,一个立即执行的匿名函数可以把函数体拆开,一般这么写:
1 2 3 4 |
(function (x) { return x * x; })(3); |
说了这么多,难道闭包就是为了返回一个函数然后延迟执行吗?
当然不是!闭包有非常强大的功能。举个栗子:
在面向对象的程序设计语言里,比如Java和C++,要在对象内部封装一个私有变量,可以用private
修饰一个成员变量。
在没有class
机制,只有函数的语言里,借助闭包,同样可以封装一个私有变量。我们用JavaScript创建一个计数器:
1 2 3 4 5 6 7 8 9 10 11 12 |
'use strict'; function create_counter(initial) { var x = initial || 0; return { inc: function () { x += 1; return x; } } } |
它用起来像这样:
1 2 3 4 5 6 7 8 9 10 |
var c1 = create_counter(); console.log(c1.inc()); // 1 console.log(c1.inc()); // 2 console.log(c1.inc()); // 3 var c2 = create_counter(10); console.log(c2.inc()); // 11 console.log(c2.inc()); // 12 console.log(c2.inc()); // 13 |
在返回的对象中,实现了一个闭包,该闭包携带了局部变量x
,并且,从外部代码根本无法访问到变量x
。换句话说,闭包就是携带状态的函数,并且它的状态可以完全对外隐藏起来。
闭包还可以把多参数的函数变成单参数的函数。例如,要计算x^y
可以用Math.pow(x, y)
函数,不过考虑到经常计算x²
或x³
,我们可以利用闭包创建新的函数pow2
和pow3
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
'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)); // 25 console.log(pow3(7)); // 343 |
3. 箭头函数
ES6标准新增了一种新的函数:Arrow Function(箭头函数)。
为什么叫Arrow Function?因为它的定义用的就是一个箭头:
1 2 |
x => x * x |
上面的箭头函数相当于:
1 2 3 4 |
function (x) { return x * x; } |
在继续学习箭头函数之前,请测试你的浏览器是否支持ES6的Arrow Function:
1 2 3 4 |
'use strict'; var fn = x => x * x; console.log('你的浏览器支持ES6的Arrow Function!'); |
箭头函数相当于匿名函数,并且简化了函数定义。箭头函数有两种格式,一种像上面的,只包含一个表达式,连{ ... }
和return
都省略掉了。还有一种可以包含多条语句,这时候就不能省略{ ... }
和return
:
1 2 3 4 5 6 7 8 9 |
x => { if (x > 0) { return x * x; } else { return - x * x; } } |
如果参数不是一个,就需要用括号()括起来:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// 两个参数: (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; } |
如果要返回一个对象,就要注意,如果是单表达式,这么写的话会报错:
1 2 3 |
// SyntaxError: x => { foo: x } |
因为和函数体的{ ... }有语法冲突,所以要改为:
1 2 3 4 |
// ok: x => ({ foo: x }) this |
箭头函数看上去是匿名函数的一种简写,但实际上,箭头函数和匿名函数有个明显的区别:箭头函数内部的this
是词法作用域,由上下文确定。
回顾前面的例子,由于JavaScript函数对this绑定的错误处理,下面的例子无法得到预期结果:
1 2 3 4 5 6 7 8 9 10 11 |
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
:
1 2 3 4 5 6 7 8 9 10 |
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写法:
1 2 |
var that = this; |
就不再需要了。
由于this
在箭头函数中已经按照词法作用域绑定了,所以,用call()
或者apply()
调用箭头函数时,无法对this
进行绑定,即传入的第一个参数被忽略:
1 2 3 4 5 6 7 8 9 10 |
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); } }; console.log(obj.getAge(2015)); // 25 |
练习
请使用箭头函数简化排序时传入的函数:
1 2 3 4 5 6 7 8 |
'use strict' var arr = [10, 20, 1, 2]; arr.sort((x, y) => { return x-y; }); console.log(arr); // [1, 2, 10, 20] |
4. 生成器
generator(生成器)是ES6标准引入的新的数据类型。一个generator看上去像一个函数,但可以返回多次。
ES6定义generator标准的哥们借鉴了Python的generator的概念和语法,如果你对Python的generator很熟悉,那么ES6的generator就是小菜一碟了。
我们先复习函数的概念。一个函数是一段完整的代码,调用一个函数就是传入参数,然后返回结果:
1 2 3 4 |
function foo(x) { return x + x; } |
var r = foo(1); // 调用foo函数
函数在执行过程中,如果没有遇到return语句(函数末尾如果没有return,就是隐含的return undefined;),控制权无法交回被调用的代码。
generator跟函数很像,定义如下:
1 2 3 4 5 6 |
function* foo(x) { yield x + 1; yield x + 2; return x + 3; } |
generator和函数不同的是,generator由function*
定义(注意多出的*号),并且,除了return
语句,还可以用yield
返回多次。
generator就是能够返回多次的“函数”?返回多次有啥用?
我们以一个著名的斐波那契数列为例,它由0,1开头:
0 1 1 2 3 5 8 13 21 34 ...
要编写一个产生斐波那契数列的函数,可以这么写:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
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] |
函数只能返回一次,所以必须返回一个Array。但是,如果换成generator,就可以一次返回一个数,不断返回多次。用generator改写如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
function* fib(max) { var t, a = 0, b = 1, n = 0; while (n < max) { yield a; [a, b] = [b, a + b]; n ++; } return; } |
直接调用试试:
1 2 |
fib(5); // fib {[[GeneratorStatus]]: "suspended", [[GeneratorReceiver]]: Window} |
直接调用一个generator和调用函数不一样,fib(5)仅仅是创建了一个generator对象,还没有去执行它。
调用generator对象有两个方法,一是不断地调用generator对象的next()方法:
1 2 3 4 5 6 7 8 |
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} |
next()
方法会执行generator的代码,然后,每次遇到yield x;
就返回一个对象{value: x, done: true/false}
,然后“暂停”。返回的value
就是yield
的返回值,done
表示这个generator是否已经执行结束了。如果done
为true
,则value
就是return
的返回值。
当执行到done
为true
时,这个generator对象就已经全部执行完毕,不要再继续调用next()
了。
第二个方法是直接用for ... of
循环迭代generator对象,这种方式不需要我们自己判断done
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
'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, ... } |
generator和普通函数相比,有什么用?
因为generator可以在执行过程中多次返回,所以它看上去就像一个可以记住执行状态的函数,利用这一点,写一个generator就可以实现需要用面向对象才能实现的功能。例如,用一个对象来保存状态,得这么写:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
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时需要这么写代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
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时可以这么写:
1 2 3 4 5 6 7 8 9 10 |
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); } |
看上去是同步的代码,实际执行是异步的。
练习
要生成一个自增的ID,可以编写一个next_id()
函数:
1 2 3 4 5 6 7 |
var current_id = 0; function next_id() { current_id ++; return current_id; } |
由于函数无法保存状态,故需要一个全局变量current_id来保存数字。
不用闭包,试用generator改写:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
'use strict'; function* next_id() { var i = 1; while (true) { yield i++; } } // 测试: var x, pass = true, g = next_id(); for (x = 1; x < 100; x ++) { if (g.next().value !== x) { pass = false; console.log('测试失败!'); break; } } if (pass) { console.log('测试通过!'); } |
学习参考教程:http://www.liaoxuefeng.com
微信扫描下方的二维码阅读本文