Lambda
在ES6的标准中称为Arrow Function(箭头函数)。下面是一个简单的箭头函数:
x => x * x
上面的定义和下面的代码定义效果一样:
1 function (x) { 2 return x * x; 3 }
箭头函数相当于匿名函数,并且简化了函数定义。箭头函数有两种格式,一种像上面的,只包含一个表达式,连{ ... }和return都省略掉了。还有一种可以包含多条语句,这时候就不能省略{ ... }和return:
1 x => { 2 if (x > 0) { 3 return x * x; 4 } 5 else { 6 return - x * x; 7 } 8 }
如果参数不是一个,就需要用括号()
括起来:
1 // 两个参数: 2 (x, y) => x * x + y * y 3 4 // 无参数: 5 () => 3.14 6 7 // 可变参数: 8 (x, y, ...rest) => { 9 var i, sum = x + y; 10 for (i=0; i<rest.length; i++) { 11 sum += rest[i]; 12 } 13 return sum; 14 }
如果要返回一个对象,就要注意,如果是单表达式,这么写的话会报错:
x => { foo: x }
因为和函数体的{ ... }有语法冲突,所以要改为:
x => ({ foo: x })
this
箭头函数看上去是匿名函数的一种简写,但实际上,箭头函数和匿名函数有个明显的区别:箭头函数内部的this
是词法作用域,由上下文确定。
回顾前面的例子,由于JavaScript函数对this
绑定的错误处理,下面的例子无法得到预期结果:
1 var obj = { 2 birth: 1990, 3 getAge: function () { 4 var b = this.birth; // 1990 5 var fn = function () { 6 return new Date().getFullYear() - this.birth; // this指向window或undefined 7 }; 8 return fn(); 9 } 10 };
现在,箭头函数完全修复了this
的指向,this
总是指向词法作用域,也就是外层调用者obj
:
1 var obj = { 2 birth: 1990, 3 getAge: function () { 4 var b = this.birth; // 1990 5 var fn = () => new Date().getFullYear() - this.birth; // this指向obj对象 6 return fn(); 7 } 8 }; 9 obj.getAge(); // 25
如果使用箭头函数,以前的那种hack写法:
var that = this;
就不再需要了。
由于this在箭头函数中已经按照词法作用域绑定了,所以,用call()或者apply()调用箭头函数时,无法对this进行绑定,即传入的第一个参数被忽略:
1 var obj = { 2 birth: 1990, 3 getAge: function (year) { 4 var b = this.birth; // 1990 5 var fn = (y) => y - this.birth; // this.birth仍是1990 6 return fn.call({birth:2000}, year); 7 } 8 }; 9 obj.getAge(2015); // 25
而Lambda则是多种语言中对箭头函数的通用叫法。
Promise
在JavaScript的世界中,所有代码都是单线程执行的。
由于这个“缺陷”,导致JavaScript的所有网络操作,浏览器事件,都必须是异步执行。
所以在编写异步操作时,一般都是使用回调或观察者模式来订阅事件,那么就会出现一种问题,当我的一个异步是基于另一个异步之后的,就会出现嵌套的代码,我们来看一个例子。
这里我们设计一个抽奖程序,用户需要连抽3次奖,每次都中才能得到大奖,有一次失败则全部失败,抽奖采用异步请求(这里我们就用setTimeout和随机数函数代替异步的网络请求),其中,第一次抽奖概率90%,第二次70%,最后一次50%但是请求的方式会变(即不能复用前两次的代码)。前两次使用func1来模拟,最后一次用func2来模拟,最后的代码如下:
1 // 假设异步调用会在 0.5 秒后执行 value 异步调用的成功率 0 - 1,如果成功调用 success,如果失败调用 fail 2 function func1(value, success, fail) { 3 setTimeout(function() { 4 var r = Math.random(); 5 if(value > r) { 6 success(); 7 } else { 8 fail(); 9 } 10 }, 500); 11 } 12 13 // 和上面的函数功能一致,只是成功率为 50% 14 function func2(success, fail) { 15 setTimeout(function() { 16 var r = Math.random(); 17 if(0.5 > r) { 18 success(); 19 } else { 20 fail(); 21 } 22 }, 1000); 23 } 24 25 26 func1(0.9, function() { 27 console.log("第1次90%成功概率,成功了,执行下一次调用"); 28 29 func1(0.7, function() { 30 console.log("第2次70%成功概率,成功了,执行下一次调用"); 31 32 func2(function() { 33 console.log("第3次50%成功概率,成功了,你运气真好,赢得了奖励"); 34 }, function() { 35 console.log("第3次50%成功概率,失败了,好可惜"); 36 }); 37 38 }, function() { 39 console.log("第2次70%成功概率,失败了,调用结束"); 40 }); 41 42 }, function() { 43 console.log("第1次90%成功概率,失败了,调用结束"); 44 });
可以通过刷新网页来多次查看抽奖的结果。
那么我们可以发现,代码中出现了3次嵌套,如果我们的抽奖要连续抽10次,就会出现10个嵌套了。
初识Promise
Promise有各种开源实现,在ES6中被统一规范,由浏览器直接支持。
下面给出ES6之前的开源项目列表,如果需要在ES6之前版本的浏览器上运行可以用到:
我们这篇笔记基于ES6规范的Promise来学习。
Promise用来干嘛的?你猜的没错,就是用来解决我们上面的异步代码编写出现嵌套的问题。
Promise最核心的技术就是提供一种类似同步代码的编写方式来编写异步代码,从而避免异步调用的嵌套导致的问题。
我们先看一看上面例子中的func2(不带参数)用Promise怎么写:
1 function func(resolve, reject) { 2 setTimeout(function() { 3 var r = Math.random(); 4 if(0.5 > r) { 5 resolve("success"); 6 } else { 7 reject("fail"); 8 } 9 }, 1000); 10 } 11 12 var p1 = new Promise(func); 13 var p2 = p1.then(function (result) { 14 console.log('成功:' + result); 15 }); 16 var p3 = p2.catch(function (reason) { 17 console.log('失败:' + reason); 18 }); 19 20 console.log(p1 === p2); // false 21 console.log(p1 === p3); // false 22 console.log(p2 === p3); // false
这个func()函数有两个参数,这两个参数都是函数,如果执行成功,我们将调用resolve,如果执行失败,我们将调用reject。可以看出,func()函数只关心自身的逻辑,并不关心具体的resolve和reject将如何处理结果。
我们创建的Promise会执行传入的函数,当调用then时传入的函数会在func函数中调用resolve时被调用,同时可以拿到传递的参数,同样catch对应的是reject函数。
另外我们可以发现,then和catch都会返回一个全新创建的Promise对象,这样我们可以采用链式调用来写:
1 new Promise(func) 2 .then(function (result) { 3 console.log('成功:' + result); 4 }) 5 .catch(function (reason) { 6 console.log('失败:' + reason); 7 });
Promise对象接受的就是这样的一个函数,没有返回值,那么如果需要传递参数该怎么办?下面再看看func1(带有参数)的Promise怎么写:
1 function func(value) { 2 return function(resolve, reject) { 3 setTimeout(function() { 4 var r = Math.random(); 5 if(value > r) { 6 resolve("success"); 7 } else { 8 reject("fail"); 9 } 10 }, 500); 11 }; 12 } 13 14 new Promise(func(0.9)) 15 .then(function (result) { 16 console.log('成功:' + result); 17 }) 18 .catch(function (reason) { 19 console.log('失败:' + reason); 20 });
我们发现使用闭包的方式,定义一个函数返回给Promise使用的函数即可实现参数的传递。
串行执行异步
串行执行即一个异步执行完毕后开始执行下一个,类似我们最开始的3重嵌套例子,使用Promise的实现如下:
1 // 假设异步调用会在 0.5 秒后执行 value 异步调用的成功率 0 - 1 2 function func1(value) { 3 return function(resolve, reject) { 4 setTimeout(function() { 5 var r = Math.random(); 6 if(value > r) { 7 resolve("success"); 8 } else { 9 reject("fail"); 10 } 11 }, 500); 12 }; 13 } 14 15 // 和上面的函数功能一致,只是成功率为 50% 16 function func2() { 17 return function(resolve, reject) { 18 setTimeout(function() { 19 var r = Math.random(); 20 if(0.5 > r) { 21 resolve("success"); 22 } else { 23 reject("fail"); 24 } 25 }, 1000); 26 }; 27 } 28 29 new Promise(func1(0.9)) 30 .then(function (result) { 31 console.log("第1次90%成功概率,成功了,执行下一次调用"); 32 // 返回新的 Promise 对象继续执行 33 return new Promise(func1(0.7)); 34 }).then(function (result) { 35 console.log("第2次70%成功概率,成功了,执行下一次调用"); 36 // 返回新的 Promise 对象继续执行 37 return new Promise(func2()); 38 }) 39 .then(function (result) { 40 console.log("第3次50%成功概率,成功了,你运气真好,赢得了奖励"); 41 // 全部执行完毕 42 }) 43 .catch(function (reason) { 44 // 可以将第几次抽奖或其它信息通过 reason 参数传递过来 45 console.log("失败了,好可惜"); 46 });
如你所见,我们使用了类似同步的代码实现了异步的逻辑执行。
并行执行异步
试想一个页面聊天系统,我们需要从两个不同的URL分别获得用户的个人信息和好友列表,这两个任务是可以并行执行的,用Promise.all()实现如下:
1 var p1 = new Promise(function (resolve, reject) { 2 setTimeout(resolve, 500, 'P1'); 3 }); 4 var p2 = new Promise(function (resolve, reject) { 5 setTimeout(resolve, 600, 'P2'); 6 }); 7 // 同时执行p1和p2,并在它们都完成后执行then: 8 Promise.all([p1, p2]).then(function (results) { 9 console.log(results); // 获得一个Array: ['P1', 'P2'] 10 });
有些时候,多个异步任务是为了容错。比如,同时向两个URL读取用户的个人信息,只需要获得先返回的结果即可。这种情况下,用Promise.race()实现:
var p1 = new Promise(function (resolve, reject) { setTimeout(resolve, 500, 'P1'); }); var p2 = new Promise(function (resolve, reject) { setTimeout(resolve, 600, 'P2'); }); Promise.race([p1, p2]).then(function (result) { console.log(result); // 'P1' });
由于p1执行较快,Promise的then()将获得结果'P1'。p2仍在继续执行,但执行结果将被丢弃。
如果我们组合使用Promise,就可以把很多异步任务以并行和串行的方式组合起来执行。