文章目录
函数
定义和调用
定义方式
function abs(x) { //abs函数名称,(x)列出参数
if (x >= 0) { //函数体
return x;
} else {
return -x;
}
}
第二种定义方式:
var abs = function (x) {
...
}; //在函数体末尾加一个;,表示赋值语句结束
JavaScript允许传入任意个参数而不影响调用,因此:
abs(-9, 'haha', 'hehe', null); // 返回9
abs(); // 返回NaN
arguments
arguments只在函数内部起作用,并且永远指向当前函数的调用者传入的所有参数。arguments类似Array但它不是一个Array:
function foo(x) {
console.log('x = ' + x); // 10
for (var i=0; i<arguments.length; i++) {
console.log('arg ' + i + ' = ' + arguments[i]); // 10, 20, 30
}
}
foo(10, 20, 30);
//x = 10
//arg 0 = 10
//arg 1 = 20
//arg 2 = 30
即使函数不定义任何参数,还是可以拿到参数的值:
function abs() {
if (arguments.length === 0) {
return 0;
}
var x = arguments[0];
return x >= 0 ? x : -x;
}
abs(); // 0
abs(10); // 10
abs(-9); // 9
arguments最常用于判断传入参数的个数。你可能会看到这样的写法:
// foo(a[, b], c)
// 接收2~3个参数,b是可选参数,如果只传2个参数,b默认为null:
function foo(a, b, c) {
if (arguments.length === 2) {
// 实际拿到的参数是a和b,c为undefined
c = b; // 把b赋给c
b = null; // b变为默认值
}
// ...
}
要把中间的参数b变为“可选”参数,就只能通过arguments判断,然后重新调整参数并赋值。
rest参数
rest参数(ES6标准引入),使用方法:
function foo(a, b, ...rest) {
console.log('a = ' + a);
console.log('b = ' + b);
console.log(rest);
}
foo(1, 2, 3, 4, 5);
// 结果:
// a = 1
// b = 2
// Array [ 3, 4, 5 ]
foo(1);
// 结果:
// a = 1
// b = undefined
// Array []
变量作用域与解构赋值
变量提升
JavaScript的函数定义会先扫描整个函数体的语句,把所有申明的变量“提升”到函数顶部:
'use strict';
function foo() {
var x = 'Hello, ' + y;
console.log(x);
var y = 'Bob';
}
foo();
虽然是strict模式,但语句var x = 'Hello, ' + y;
并不报错,原因是变量y在稍后申明了。但是console.log
显示Hello, undefined
,说明变量y的值为undefined。这正是因为JavaScript引擎自动提升了变量y的声明,但不会提升变量y的赋值。
JavaScript引擎看到的代码相当于:
function foo() {
var y; // 提升变量y的申明,此时y为undefined
var x = 'Hello, ' + y;
console.log(x);
y = 'Bob';
}
名字空间
全局变量会绑定到window上,不同的JavaScript文件如果使用了相同的全局变量,或者定义了相同名字的顶层函数,都会造成命名冲突,并且很难被发现。
减少冲突的一个方法是把自己的所有变量和函数全部绑定到一个全局变量中。例如:
// 唯一的全局变量MYAPP:
var MYAPP = {};
// 其他变量:
MYAPP.name = 'myapp';
MYAPP.version = 1.0;
// 其他函数:
MYAPP.foo = function () {
return 'foo';
};
把自己的代码全部放入唯一的名字空间MYAPP中,会大大减少全局变量冲突的可能。
局部作用域
for循环中无法定义局部作用域的变量,ES6引入let
,可以用let
代替var
申明块级作用域变量:
function foo() {
var sum = 0;
for (let i=0; i<100; i++) {
sum += i;
}
// 用var不会报错,用let则会SyntaxError:
i += 1;
}
常量
ES6前不能申明,通常用全部大写的变量来表示。ES6标准引入了新的关键字const
来定义常量,const
与let
都具有块级作用域:
const PI = 3.14;
PI = 3; // 某些浏览器不报错,但是无效果!
PI; // 3.14
let、var区别总结:
var
之前说过只能用var申明一次,但申明多次不会报错;变量声明提前
let
可以定义块级作用域变量;没有变量提升;有暂时性死区;不能重复声明(将报错Uncaught SyntaxError: Identifier 'xxx' has already been declared
)
ES6 明确规定,如果区块中存在let和const命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。
总之,在代码块内,使用let命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,简称 TDZ)。
附上代码加强理解:
//let不能在一个作用域内重复声明;产生块级作用域
{
let a = 1;
let a = 2; //报错
var b = 2;
}
console.log(a);//报错
console.log(b);//2
//let与for的独特应用,此处知识点:setTimeout机制,JS事件循环机制
for (var i = 0; i <10; i++) {
setTimeout(function() { // 同步注册回调函数到 异步的 宏任务队列。
console.log(i); // 执行此代码时,同步代码for循环已经执行完成
}, 0); ///输出:10
for (let i = 0; i < 10; i++) {
setTimeout(function() {
console.log(i); // i 是循环体内局部作用域,不受外界影响。
}, 0); //输出:0 1 2 3 4 5 6 7 8 9
//let的暂时性死域
let a = 1;
let b = 5;
{
console.log(a);//报错,暂时性死域,在let a前面使用a不合法
let a = 2;
console.log(a); //2
console.log(b); //5,说明可以访问父块级作用域变量a
}
解构赋值
将数组元素分别赋值给几个变量传统做法:
var array = ['hello', 'JavaScript', 'ES6'];
var x = array[0];
var y = array[1];
var z = array[2];
解构赋值:
var [x, y, z] = ['hello', 'JavaScript', 'ES6'];
console.log('x = ' + x + ', y = ' + y + ', z = ' + z);
//如果数组本身还有嵌套,注意嵌套层次和位置要保持一致:
let [x, [y, z]] = ['hello', ['JavaScript', 'ES6']];
//可以忽略某些元素:
let [, , z] = ['hello', 'JavaScript', 'ES6']; // 忽略前两个元素,只对z赋值第三个元素
z; // 'ES6'
如果需要从一个对象中取出若干属性,也可以使用解构赋值,便于快速获取对象的指定属性:
var person = {
name: '小明',
age: 20,
gender: 'male',
passport: 'G-12345678',
address: {
city: 'Beijing',
zipcode: '100001'
}
};
var {name, age, passport} = person;
console.log('name = ' + name + ', age = ' + age + ', passport = ' + passport);
// name = 小明, age = 20, passport = G-12345678
//直接对嵌套的对象属性赋值
var {name, address: {city, zip}} = person;
name; // '小明'
city; // 'Beijing'
zip; // undefined, 因为属性名是zipcode而不是zip
// 注意: 此处的address不是变量,而是为了让city和zip获得嵌套的address对象的属性:
address; // Uncaught ReferenceError: address is not defined
//要使用的变量名和属性名不一致:
let {name, passport:id} = person;
name; // '小明'
id; // 'G-12345678'
// 注意: 此处的passport不是变量,而是为了让变量id获得passport属性:
passport; // Uncaught ReferenceError: passport is not defined
//使用默认值避免返回undefined
// 如果person对象没有single属性,默认赋值为true:
var {name, single=true} = person;
name; // '小明'
single; // true
如果变量已经被声明了,再次赋值的时候,正确的写法也会报语法错误:
// 声明变量:
var x, y;
// 解构赋值:
{x, y} = { name: '小明', x: 100, y: 200};
// 语法错误: Uncaught SyntaxError: Unexpected token =
这是因为JavaScript引擎把{
开头的语句当作了块处理,于是=
不再合法。解决方法是用小括号()
括起来:
({x, y} = { name: '小明', x: 100, y: 200});
使用场景:
交换变量:
var x=1, y=2;
[x, y] = [y, x]
快速获取当前页面的域名和路径:
var {hostname:domain, pathname:path} = location;
如果一个函数接收一个对象作为参数,那么可以使用解构直接把对象的属性绑定到变量中。例如,下面的函数可以快速创建一个Date对象:
function buildDate({year, month, day, hour=0, minute=0, second=0}) {
return new Date(year + '-' + month + '-' + day + ' ' + hour + ':' + minute + ':' + second);
}
它的方便之处在于传入的对象只需要year、month和day这三个属性:
buildDate({ year: 2017, month: 1, day: 1 });
// Sun Jan 01 2017 00:00:00 GMT+0800 (CST)
也可以传入hour、minute和second属性:
buildDate({ year: 2017, month: 1, day: 1, hour: 20, minute: 15 });
// Sun Jan 01 2017 20:15:00 GMT+0800 (CST)
方法
var xiaoming = {
name: '小明',
birth: 1990,
age: function () {
var y = new Date().getFullYear();
return y - this.birth;
}
};
this
是一个特殊变量,始终指向当前对象,也就是xiaoming这个变量。this.birth
可以拿到xiaoming的birth属性。
用var that = this;
,可以在方法内部定义其他函数,而不是把所有语句都堆到一个方法中。
var xiaoming = {
name: '小明',
birth: 1990,
age: function () {
var that = this; // 在方法内部一开始就捕获this
function getAgeFromBirth() {
var y = new Date().getFullYear();
return y - that.birth; // 用that而不是this
}
return getAgeFromBirth();
}
};
xiaoming.age(); // 25
参考网址:https://www.liaoxuefeng.com/wiki/1022910821149312/1023023754768768
apply
要指定函数的this指向哪个对象,可以用函数本身的apply方法,它接收两个参数,第一个参数就是需要绑定的this变量,第二个参数是Array,表示函数本身的参数。
用apply修复getAge()
调用:
function getAge() {
var y = new Date().getFullYear();
return y - this.birth;
}
var xiaoming = {
name: '小明',
birth: 1990,
age: getAge
};
xiaoming.age(); // 25
getAge.apply(xiaoming, []); // 25, this指向xiaoming, 参数为空
另一个与apply()
类似的方法是call()
,唯一区别是:
apply()
把参数打包成Array再传入;call()
把参数按顺序传入。
比如调用Math.max(3, 5, 4)
,分别用apply()
和call()
实现如下:
Math.max.apply(null, [3, 5, 4]); // 5
Math.max.call(null, 3, 5, 4); // 5
对普通函数调用,我们通常把this
绑定为null
。
装饰器
利用apply()
,我们还可以动态改变函数的行为。
JavaScript的所有对象都是动态的,即使内置的函数,我们也可以重新指向新的函数。
现在假定我们想统计一下代码一共调用了多少次parseInt()
,可以把所有的调用都找出来,然后手动加上count += 1
,不过这样做太傻了。最佳方案是用我们自己的函数替换掉默认的parseInt()
:
var count = 0;
var oldParseInt = parseInt; // 保存原函数
window.parseInt = function () {
count += 1;
return oldParseInt.apply(null, arguments); // 调用原函数
};
parseInt('10');
parseInt('20');
parseInt('30');
console.log('count = ' + count); // 3
高阶函数
map/reduce
map
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]
var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9];
arr.map(String); // ['1', '2', '3', '4', '5', '6', '7', '8', '9']
reduce
[x1, x2, x3, x4].reduce(f) = f(f(f(x1, x2), x3), x4)
var arr = [1, 3, 5, 7, 9];
arr.reduce(function (x, y) {
return x + y;
}); // 25
var arr = [1, 3, 5, 7, 9];
arr.reduce(function (x, y) {
return x * 10 + y;
}); // 13579
把一个字符串13579先变成Array——[1, 3, 5, 7, 9],再利用reduce()
就可以写出一个把字符串转换为Number的函数。
练习:不要使用JavaScript内置的parseInt()
函数,利用map和reduce操作实现一个string2int()
函数:
function string2int(s) {
var arr=s.split(''); //["1", "2", "3", "4", "5"]
arr=arr.map(s=>{
return +s; //[1, 2, 3, 4, 5];测试后发现如果为-号则会变为[-1,-2,-3,-4,-5]
});
var k=arr.reduce(function(x,y){
return x*10+y;
});
return k;
}
练习:请把用户输入的不规范的英文名字,变为首字母大写,其他小写的规范名字。输入:[‘adam’, ‘LISA’, ‘barT’],输出:[‘Adam’, ‘Lisa’, ‘Bart’]。
function normalize(arr) {
arr=arr.map(s=>{
s=s.toLowerCase();
s=s.substring(0,1).toUpperCase()+s.substring(1);
return s;
});
return arr;
}
注意,考虑:
["1", "2", "3"].map(parseInt)
虽然可能有人期望[1, 2, 3],但实际结果是[1, NaN, NaN]。
个人理解:
parseInt(String s, int radix)
,通常与一个参数一起使用,但需要两个参数。一是表达式,二是回调函数的基数。
传入的radix
表明s
是几进制数,最后输出返回10进制数。
parseInt('111',2); //7
parseInt('12',2); //1
parseInt('A',16); //10
Array.prototype.map
传递3个参数:元素、索引、数组。
那么第一次map传递'1',0,数组
,parseInt执行parseInt('1',0)
得到1
;
第二次map传递'2',1,数组
,parseInt执行parseInt('2',1)
得到NaN
;
第三次map传递'3',2,数组
,parseInt执行parseInt('3',2)
得到NaN
。
['10','10','10'].map(parseInt); //10,NaN,2
//此处第三次map传递'10',2,数组,parseInt执行parseInt(‘10’,2)得到2
参考网址:https://www.liaoxuefeng.com/wiki/1022910821149312/1024322552460832
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map
parseInt():https://www.runoob.com/java/number-parseint.html
filter
filter用于过滤筛选元素然后返回剩下的元素。
删掉偶数,只保留奇数:
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()方法
}); //s.trim()是把字符串中头尾的空格去掉,此处是看去掉头尾空格后字符串是否有内容,如果有则返回true保留
r; // ['A', 'B', 'C']
巧妙地去除Array的重复元素:
var
r,
arr = ['apple', 'strawberry', 'banana', 'pear', 'apple', 'orange', 'orange', 'strawberry'];
r = arr.filter(function (element, index, self) {
return self.indexOf(element) === index; //indexOf返回字符串值在字符串中首次出现的位置
});
sort
通常规定,对于两个元素x和y,如果认为x < y,则返回-1,如果认为x == y,则返回0,如果认为x > y,则返回1。
// 看上去正常的结果:
['Google', 'Apple', 'Microsoft'].sort(); // ['Apple', 'Google', 'Microsoft'];
// apple排在了最后:
['Google', 'apple', 'Microsoft'].sort(); // ['Google', 'Microsoft", 'apple']
// 无法理解的结果:
[10, 20, 1, 2].sort(); // [1, 10, 2, 20]
按数字大小排序,我们可以这么写:
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]
//倒序排序
arr.sort(function (x, y) {
if (x < y) {
return 1;
}
if (x > y) {
return -1;
}
return 0;
}); // [20, 10, 2, 1]
排序时忽略大小写,按字母序排序:
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
every
every()
方法可以判断数组的所有元素是否满足测试条件。
var arr = ['Apple', 'pear', 'orange'];
console.log(arr.every(function (s) {
return s.length > 0;
})); // true, 因为每个元素都满足s.length>0
console.log(arr.every(function (s) {
return s.toLowerCase() === s;
})); // false, 因为不是每个元素都全部是小写
find
find()
方法用于查找符合条件的第一个元素,如果找到了,返回这个元素,否则,返回undefined
:
var arr = ['Apple', 'pear', 'orange'];
console.log(arr.find(function (s) {
return s.toLowerCase() === s;
})); // 'pear', 因为pear全部是小写
console.log(arr.find(function (s) {
return s.toUpperCase() === s;
})); // undefined, 因为没有全部是大写的元素
findIndex
findIndex()
查找符合条件的第一个元素,返回这个元素的索引,如果没有找到,返回-1:
var arr = ['Apple', 'pear', 'orange'];
console.log(arr.findIndex(function (s) {
return s.toLowerCase() === s;
})); // 1, 因为'pear'的索引是1
console.log(arr.findIndex(function (s) {
return s.toUpperCase() === s;
})); // -1
forEach
forEach()
和map()
类似,它也把每个元素依次作用于传入的函数,但不会返回新的数组。forEach()
常用于遍历数组,因此,传入的函数不需要返回值:
var arr = ['Apple', 'pear', 'orange'];
arr.forEach(console.log); // 依次打印每个元素
IIEF
全称:Immediately-Invoked Function Expression(立即执行函数表达式)
作用:隐藏实现;不会污染外部(全局)命名空间;
(function() { //匿名函数自调用
console.log('...');
})()
(function(){
var a = 1;
function test(){
console.log(++a);
}
windows.$=function(){
return{
test:test
}}})()
$().test();
可能会因为js中自动分号而引发问题,下面代码如果没有第二行分号,可能被解析为c(),所以有时使用;(function(){})();
:
a = b + c
;(function() {
// code
})();
闭包
函数作为返回值
不返回求和的结果,而是返回求和的函数!
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
f1()
和f2()
的调用结果互不影响。
闭包
一定要引用循环变量怎么办?方法是再创建一个函数,用该函数的参数绑定循环变量当前的值,无论该循环变量后续如何更改,已绑定到函数参数的值不变:
function count() {
var arr = [];
for (var i=1; i<=3; i++) {
arr.push((function (n) {
return function () {
return n * n;
}
})(i)); //调用这个匿名函数并传入参数i(这个匿名函数接受一个参数命名为n)
}
return arr;
}
var results = count();
var f1 = results[0];
var f2 = results[1];
var f3 = results[2];
f1(); // 1
f2(); // 4
f3(); // 9
(function (x) {
return x * x;
})(3); // 9
闭包可以封装私有变量:
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
将多参数函数变成单参数函数:
function make_pow(n) {
return function (x) {
return Math.pow(x, n); //计算x^n
}
}
var pow2 = make_pow(2);
var pow3 = make_pow(3);
console.log(pow2(5)); // 25
console.log(pow3(7)); // 343
很久很久以前,有个叫阿隆佐·邱奇的帅哥,发现只需要用函数,就可以用计算机实现运算,而不需要0、1、2、3这些数字和+、-、*、/这些符号。
// 定义数字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));
}
}
}
// 计算数字2 = 1 + 1:
var two = add(one, one);
// 计算数字3 = 1 + 2:
var three = add(one, two);
// 计算数字5 = 2 + 3:
var five = add(two, three);
// 你说它是3就是3,你说它是5就是5,你怎么证明?
// 给3传一个函数,会打印3次:
(three(function () {
console.log('print 3 times');
}))();
// 给5传一个函数,会打印5次:
(five(function () {
console.log('print 5 times');
}))();
图片为自我理解:
闭包缺点:变量未释放导致占用内存时间变长;易造成内存泄漏。
常见的内存泄露:意外的全局变量、没有及时清理计时器或回调函数、闭包.
练习:
function fun(n,o) {
console.log(o);
return {
fun:function(m){
return fun(m,n);
}}}
var a = fun(0); a.fun(1); a.fun(2); a.fun(3);
//undefined,0,0,0
var b = fun(0).fun(1).fun(2).fun(3);
//undefined,0,1,2
var c = fun(0).fun(1); c.fun(2); c.fun(3);
//undefined,0,1,1
箭头函数
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;
}
返回一个对象
x => ({ foo: x }) //括号不能忘
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
generator
generator(生成器)跟函数很像,定义如下:
function* foo(x) {
yield x + 1;
yield x + 2;
return x + 3;
}
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]
函数只能返回一次,所以必须返回一个Array。但是,如果换成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;
}
fib(5); // fib {[[GeneratorStatus]]: "suspended", [[GeneratorReceiver]]: Window}
直接调用一个generator和调用函数不一样,fib(5)
仅仅是创建了一个generator对象,还没有去执行它。
调用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}
next()
方法会执行generator的代码,然后,每次遇到yield x;
就返回一个对象{value: x, done: true/false}
,然后“暂停”。返回的value就是yield的返回值,done表示这个generator是否已经执行结束了。如果done为true,则value就是return的返回值。
第二个方法是直接用for ... of
循环迭代generator对象,这种方式不需要我们自己判断done:
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, ...
}
练习:生成自增ID
function* next_id() {
var id = 1;
for(var i = 1;i<1000;i++){
yield id++;
}
}
写一个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);
}
看上去是同步的代码,实际执行是异步的。