Promise
首先来说一下应该明白Promise是做什么的,JS是一门典型的异步单线程的编程语言,单线程就不说了,异步如何理解呢?异步编程可以理解成在执行指令之后不能马上得到结果,而是继续执行后面的指令,等到特定的事件触发之后才得到想要的结果。 也可以和同步对比理解,同步就是一步步执行指令,而异步在遇到特殊任务(异步任务)时,并不会等待该任务执行完成之后再去执行下一步,而是跳过等待,先去执行下一步任务,等到某个时机该特殊任务(异步任务)执行完成之后再返回来对其进行处理。 比如:
console.log(1);
setTimeout(()=>{
console.log(2)
},1000)
console.log(3);
该段代码不会按照顺序输出1,2,3而是先输出1,3之后再输出2。你可能会说,最后输出2是因为它是延时1s之后才输出的。那么请看以下代码:
console.log(1);
setTimeout(()=>{
console.log(2)
},0)
console.log(3);
以上代码输出顺序依旧是1,3,2。实际上并不是因为延时导致的2最后输出而是因为这就是一个典型的异步任务,它是在同步任务之后才去执行的。
说起异步肯定首先会想到ajax,因为ajax天生就是异步操作,先看ajax伪代码
var xhr = new XMLHttpRequest();
xhr.open(method,url);
xhr.send();
xhr.onreadystatechange = function(){
if(xhr.readyState == 4 && xhr.status == 200){
document.getElementById('app').innerHTML = xhr.responseText
}
}
由于ajax是异步操作,所以我们只能将从服务器获取数据之后再渲染页面的整个过程放在ajax请求成功之后监听函数onreadystatechange里,这样做有些糟糕,会导致代码比较乱。
因此有一种操作对于异步编程来说比较优雅就是***回调函数***。
还拿ajax举例:
//简易封装ajax
function getAppJson(cb){
var xhr = new XMLHttpRequest();
xhr.open(method,url);
xhr.send();
xhr.onreadystatechange = function(){
if(xhr.readyState == 4 && xhr.status == 200){
cb(responseText)
}
}
}
//回调函数
function initHtml(val){
document.getElementById('app').innerHTML = val
}
// 将initHtml函数作为参数传入getAppJson中并在xhr.onreadystatechange 方法中执行并把后台所得数据作为参数传递给initHtml;
getAppJson(initHtml);
上述代码使用回调函数优雅的解决了ajax异步操作。
上述代码简化之后就是下面这个样子的:
function app(cb){
cb(1)
}
function init(val){
console.log(val) //1
}
app(init)
回调函数是一种很好地异步编程思想。但是有一种被人们称为***回调地狱***的情况让人们不得不使用Promise去替代回调函数。
回调地狱就是回调函数里嵌套回调函数,比如:
var sayhello = function (name, callback) {
console.log(name);
callback();
}
sayhello("first", function () {
sayhello("second", function () {
sayhello("third", function () {
console.log("end");
});
});
});
// first second third end
比如当第一个ajax获取到的id需要作为第二个ajax的请求参数使用这样也会形成回调地狱。
为了解决回调地狱的恐怖,因此呢,Promise便横空出世。
Promise 是异步编程的一种解决方案,比传统的回调函数更合理,更强大。在ES6中被标准化。
Promise的好处就是链式调用(chaining)
Promise的使用
- Promise的状态 Promise的状态表示此时异步执行的状态。Promise一共有三种状态:
- pending:初始状态
- fulfilled:操作成功状态
- rejected:操作失败状态
特点: Promise三个状态不受外界影响,一旦状态改变,就不会再发生变化,Promise对象的状态改变只有两种可能,从pening变为fulfilled和从pending变为rejected.
- 基本用法 ES6规定,Promise对象是一个构造函数,用来生成Promise实例。
let promise = new Promise((resolve,reject)=>{
//...异步代码
if(/* 异步成功 */){
resolve(value)
}else{
reject(error)
}
})
Promise构造函数接收一个函数作为参数,该函数有两个参数分别为resolve和reject。它们是两个函数,由JavaScript引擎提供,不用自己部署。
其中 resolve的作用是将Promise对象的状态从“未完成”变为“成功”(即从pending变为resolved),在异步操作成功时调用,并将异步操作的结果作为参数传递出去。
reject函数的作用是将Promise对象的状态从“未完成”变为“失败”(即从pending变为rejected),在异步操作失败时调用,并将异步操作报出的错误作为参数传递出去。
var flag = false;
var p = new Promise((resolve,reject)=>{
if(flag){
resolve(1)
}else{
reject(2)
}
})
p.then((val)=>{
console.log(val,'then')
}).catch((val)=>{
console.log(val,'catch') //2 "catch"
})
上述代码以flag模拟异步结果成功或失败,当成功时(flag为true),调用resolve并将结果1作为参数传递出去,Promise的实例化p调用Promise原型上的方法then。 then方法包含两个参数:onfulfilled 和 onrejected,它们都是 Function 类型。当Promise状态为fulfilled时,调用 then 的 onfulfilled 方法,当Promise状态为rejected时,调用 then 的 onrejected 方法,默认一般都只写一个参数。 当执行resolve时可以在then的函数参数中接收从resolve中传递过来的参数 当执行reject时可以在catch的函数参数中接收从reject中传递的参数。
由于Promise.prototype.then(onFulfilled, onRejected)和Promise.prototype.catch(onRejected)都会返回一个新的Promise对象,所以可以实现链式调用。
比如:用Promise实现两个数相加,再拿两个相加的数的结果减去一个数
function Add(a,b){
return new Promise((resolve,reject)=>{
resolve(a+b)
})
}
function Min(c){
return new Promise((resolve,reject)=>{
resolve(c-1)
})
}
Add(1,2).then((val)=>{
return Min(val)
})
.then((val)=>{
console.log(val)
})
以上可以很直观的看出Promise的链式调用。
Promise原型上还有一个常用方法叫finally.
var flag = false;
var p = new Promise((resolve,reject)=>{
if(flag){
resolve(1)
}else{
reject(2)
}
})
p.then((val)=>{
console.log(val,'then')
}).catch((val)=>{
console.log(val,'catch')
}).finally(()=>{
console.log(12345)
})
finally方法接收一个参数是一个函数,该函数无参数,该方法表示无论最终Promise对象执行resolve还是reject. finally都会执行,它表示Promise对象执行结束了。
- Promise 方法
Promise.all(iterable)
const p = Promise.all([p1,p2,p3])
let p1 = new Promise((resolve,reject)=>{
resolve(1)
})
let p2 = new Promise((resolve,reject)=>{
resolve(2)
})
let p3 = new Promise((resolve,reject)=>{
resolve(3)
})
Promise.all([p1,p2,p3]).then((val)=>{
console.log(val) //[1,2,3]
})
Promise.all()方法接收一个数组作为参数,参数也可以不是数组,但必须具有迭代器接口,且返回的每个成员都是Promise实例。 p的状态有p1,p2,p3决定,分成两种情况, (1)当p1,p2,p3的状态都为fulfilled,p的状态才会变成fulfilled,此时p1,p2,p3的返回值组成一个数组,传递给p的then方法第一个函数参数的参数。
(2)当p1,p2,p3中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的catch方法函数参数的参数。 如下:
let p1 = new Promise((resolve,reject)=>{
resolve(1)
})
let p2 = new Promise((resolve,reject)=>{
reject(2)
})
let p3 = new Promise((resolve,reject)=>{
resolve(3)
})
Promise.all([p1,p2,p3]).then((val)=>{
console.log(val)
}).catch((err)=>{
console.log(err) //2
})
总结:只有Promise.all()中所有的参数状态都是fulfilled,p的状态即为成功,当其中有一个rejected,p的状态即为失败。
Promise.race()
Promise.race(iterable)
Promise.race()方法同样将多个Promise实例包装成一个新的Promise实例,当iterable参数里的任意一个子promise被成功或失败后,父promise马上也会用子promise的成功返回值或失败详情作为参数调用父promise绑定的相应句柄,并返回该promise对象。
let p1 = new Promise((resolve,reject)=>{
resolve(1)
})
let p2 = new Promise((resolve,reject)=>{
resolve(2)
})
let p3 = new Promise((resolve,reject)=>{
resolve(3)
})
Promise.race([p1,p2,p3]).then((val)=>{
console.log(val) //1
}).catch((err)=>{
console.log(err)
})
由于p1排在第一个因此率先执行,所以当p1执行完成之后就直接停止执行p2,p3。
总结:race顾名思义比赛,执行最快的那个首先执行被返回,此时Promise状态已被确定。
Promise.resolve(value)
将一个普通的value转换成Promise对象。 返回一个状态由给定value决定的Promise对象。如果该值是thenable(即,带有then方法的对象),返回的Promise对象的最终状态由then方法执行决定;否则的话(该value为空,基本类型或者不带then方法的对象),返回的Promise对象状态为fulfilled,并且将该value传递给对应的then方法。
Promise.resolve("foo")
//等价于
new Promise(resolve=>resolve('foo'))
最后再来看一个Promise封装的Ajax:
function getJson(){
return new Promise((resolve,reject)=>{
var xhr = new XMLHttpRequest();
xhr.open(method,url);
xhr.send();
xhr.onreadystatechange = function(){
if(xhr.readyState == 4 && xhr.status == 200){
resolve(xhr.responseText)
}
}
})
}
getJson.then((data)=>{
document.getElementById("app").innerHTML = data;
})
面试题:
Promise.all()中有一个异步抛出异常,最先抛出的异常会被.then的第二个参数或者.catch补捕获。并终止程序。并且获取不到Promise.all()其他正常的结果。那么如何获取成功的请求结果呢?
(1). 在异步请求中定义自己的catch方法,一旦它被rejected,并不会触发Promise.all()的catch方法。
var flag = true;
var p1 = new Promise((resolve,reject)=>{ //p1resolved
if(flag){
resolve("p1")
}else{
reject("p1 err")
}
})
var p2 = new Promise((resolve,reject)=>{ //p2rejected
if(!flag){
resolve("p2")
}else{
reject("p2 err")
}
}).then((data)=>{
console.log(data,'p2 then')
}).catch((data)=>{
console.log(data,'p2 catch') //p2 err p2 catch
})
var p3 = new Promise((resolve,reject)=>{ //p3resolved
if(flag){
resolve("p3")
}else{
reject("p3 err")
}
})
Promise.all([p1,p2,p3]).
then((data)=>{
console.log(data); // ["p1", undefined, "p3"]
})
.catch((data)=>{
console.log(data)
})
上面代码中,p2会rejected,但是p2有自己的catch方法,该方法返回的是一个新的Promise实例,p2实际指向的是这个实例。该实例执行完catch方法后,也会resolved,导致Promise.all()方法参数里面的三个实例都会resolved,因此会调用then方法指定的回调函数,而不会调用catch方法指定的回到函数。
上面代码Promise.all()的then中输出["p1", undefined, "p3"]从中可以获取到执行成功的p1结果,p3结果。
如果p2没有自己的catch方法,那么就会调用Promise.all()的catch方法。
(2). 在Promise.all()的所有参数中无论resolved还是rejected都执行resolve.
var flag = true;
var p1 = new Promise((resolve,reject)=>{
if(flag){
resolve("p1")
}else{
reject("p1 err")
}
})
var p2 = new Promise((resolve,reject)=>{
if(!flag){
resolve("p2")
}else{
resolve("error")
}
})
var p3 = new Promise((resolve,reject)=>{
if(flag){
resolve("p3")
}else{
reject("p3 err")
}
})
Promise.all([p1,p2,p3]).
then((data)=>{
console.log(data); // ["p1", "error", "p3"]
})
.catch((data)=>{
console.log(data)
})
这个很好理解,在p2中无论成功失败都走resolve,这样就顺理成章的走到了Promise.all()的then中,此时可以获取到所有的结果,从中获取返回正常的结果。
上面代码结果是["p1", "error", "p3"],从中过滤掉失败的就好了。
参考文章: MDN Promise 深入理解 ES6 Promise
async/await
async/await和Promise一样目的都是为了异步调用的“扁平化”。async/await是非常好用的语法糖。可以认为是基于Promise的针对异步更优雅的解决方案。
用法:
- async用于申明一个函数是异步的,它的返回值是一个Promise对象。如果在函数中return 一个直接量,async会把这个直接;量通过Promise.resolve()封装成Promise对象。
//在函数前加上async关键字,表明该函数是异步函数
async function testAsync(){
return "async"
}
const result = testAsync();
console.log(result);//Promise {<resolved>: "async"}
result.then((data)=>{
console.log(data) //async
})
- 配合await await只能出现在async函数中。 await表示等待。等待异步方法执行结束。 async函数返回一个Promise对象,所以await等待async函数的返回值。也可以等待任意表达式的结果。 例如:
function func(){ //普通函数
return "hello world"
}
async function testAsync(){ //async函数
return Promise.resolve("hello async")
}
async function test(){
const result1 = await func();
const result2 = await testAsync();
console.log(result1,result2)
}
test(); // hello world hello async
await等待的值为一个Promise对象,或者其它值 await 是个运算符,用于组成表达式,await表达式的运算结果取决于它等的东西。
如果它等到的不是一个Promise对象,那await表达式的运算结果就是它等到的东西。
如果它等到的是一个Promise对象,那么await就会阻塞后面代码,等着Promise对象状态改变,比如resolve,然后得到resolve的值,作为await表达式的运算结果。
- async/await 写法与Promise比较
//Promise
function setTime(){
return new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve("setTime")
},1000)
})
}
setTime().then((data)=>{
console.log(data); // setTime 1s之后会输出
})
//async/await
function setTime(){
return new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve("setTime")
},1000)
})
}
async function test(){
const result = await setTime(); // 该段代码执行结束之前后面的代码是不会执行的。
console.log(1); // 1 1s之后输出
console.log(result) // setTime 1s之后输出
}
test();
怎么说呢,async/await就是一个语法糖,看起来貌似很难,很高深,其实用起来了也就慢慢熟悉了。感觉async看起来更直观一些吧。
展开(spread)运算符和剩余(Rest)运算符
展开运算符用三个点(...)表示,可以将数组转为逗号分隔的参数序列。化少为多。
var arr = [1,2,3]
console.log(...arr); // 1 2 3
可用于数组合并
var arr1 = [1,2,3]
var arr2 = [4,5,6]
var arr3 = [...arr1, ...arr2];
console.log(arr3); //[1,2,3,4,5,6]
可用于对象合并
var obj1 = {a:1,b:2}
var obj2 = {c:3,d:4}
var obj3 = {...obj1,...obj2}
console.log(obj3)//{a:1,b:2,c:3,d:4}
剩余运算符也是用三个点表示(...),剩余运算符会将多余元素收集压缩成一个单一的元素。
可以用来表示形参。
function func(a,...args){
console.log(a); //1
console.log(args); //[2,3,4]
}
func(1,2,3,4);
解构赋值:
const [num,...other] = [1,2,3,4,5];
console.log(num);// 1
console.log(other); [2,3,4,5]
纯函数
定义:一个函数的返回结果只依赖他的参数,并且在执行过程中没有副作用,我们就把这个函数叫做纯函数。
由定义可知纯函数有两个重要点:
- 函数的返回结果只依赖它的参数
- 函数执行过程中没有副作用
var a = 1;
function foo (b){
return a+b
}
foo(2) // 3
foo函数不是一个纯函数,因为它返回的结果依赖外部变量a,我们在不知道a的值得情况下,并不能保证foo(2)的返回值是3。虽然foo函数的代码实现并没有变化,传入的参数并没有变化,但他的返回值却是不可预料的,因为函数依赖的外部值a是不可预料的,它是1,也可能在函数foo执行之前在其他逻辑中被修改。
修改一下函数foo的逻辑:
var a = 1;
function foo (x,b){
return x+b
}
foo(1,2) // 3
现在foo的返回结果只依赖于它的参数x和b,foo(1,2)的返回结果永远是3,不管外部代码怎么变化,foo(1,2)永远是3。只要foo代码不改变,只要foo代码不改变,你传入的参数是确定的,那么foo(1,2)的值永远是可预料的。
这就是纯函数的第一个条件,一个函数的返回结果只依赖于它的参数。
再来看看第二个特点,函数执行过程没有副作用。 一个函数执行过程中对函数外部产生了可观察的变化,那么就说这个函数是有副作用的。
var a = 1;
var obj = {x:2}
function foo (obj,b){
obj.x = b
return obj.x+b
}
foo(obj ,3) // 6
obj // {x:3}
对象obj的属性x默认为2,foo执行后obj.x成为了foo函数的第二个参数,因此obj最终由{x:2}变为了{x:3},因此foo函数的执行对外部的obj产生了影响,它产生了副作用,因为它修改了外部传进来的对象,因此现在它不是一个纯函数。
function foo(b){
const obj = {x:1}
obj.x = 2;
return obj.x + b
}
以上函数foo虽然内部修改了变量obj,但obj是内部变量,外部程序观察不到,修改obj并不会产生外部可观察变化,这个函数没有副作用,因此它是一个纯函数。
除了修改外部的变量,一个函数在执行过程中还有很多方式产生外部可观察的变化,比如说调用DOM API修改页面,或者发送Ajax请求,调用BOM API等,甚至使用console.log()往控制台打印数据也是副作用。
纯函数很严格,除了计算数据以外什么都不能干,计算的时候还不能依赖除了函数参数以外的数据。
总结:一个函数的返回结果只依赖于他的参数,并且在执行过程中没有副作用,那么我们就称这个函数为纯函数。
纯函数的好处就是它非常靠谱,执行一个纯函数,它的执行结果一般都是在你的预料之中,不必担心一个纯函数会干什么坏事,他不会产生不可预料的行为,也不会对外部产生影响,不管何时何地,你给它什么,它就吐出什么。
纯函数也是函数式编程的一个很重要的概念,很多类库,框架也都有使用到,比如redux,react高阶组件因此需要了解其概念。
严格模式
使用:"use strict",可以在整个代码块前加“use strict”使用,也可以只在函数中局部添加“use strict”去使用。
特点:
- 变量必须声明才可使用。
- 创设eval作用域,除了全局作用域,函数作用域外,新增eval作用域。
- with()被禁用,with语句用于设置代码在特定对象中的作用域。
- caller/callee被禁用。
- delete使用在var 声明的变量活挂在window的变量上会报错。
- delete不可删除属性的对象时会报错。
- 对一个对象的只读属性进行赋值会报错。
- 对象有重名属性将报错。
- 函数有重名的参数会报错。
- arguments严格定义为参数,不再与形参绑定。
- 函数中的this不再指向window。
高阶函数
什么是高阶函数:
- 如果一个函数的参数是一个函数(回调函数)
- 如果一个函数的返回值是一个函数,当前这个函数也是一个高阶函数
应用场景:扩展业务代码
function Eat(a,b){ //核心业务代码
console.log(a,b)
}
Function.prototype.before = function(callback){ //高阶函数
return (...args)=>{ //使用rest运算符接收
callback();
this(...args); //使用展开运算符传入
}
}
let beforeEat = Eat.before(function(){ //自己扩展业务代码
console.log("before eat")
})
beforeEat("666","999") //传参
函数柯里化 例子:判断数据类型
- typeof 不能判断对象类型
- constructor 判断该数据是由谁构造出来
- instanceof 判断谁是谁的实例__proto__
- Object.prototype.toString.call() 缺陷不能细分谁是谁的实例
function isType(value,type){
return Object.prototype.toString.call(value) === `[object ${type}]`
}
//调用
isType([],"Array") // 判断数据 [] 的类型是否似乎数组
isType("","Array") // 判断数据 "" 的类型是否是数组
上述实现有缺陷,就是每次判断数据时都需要重复去指定type类型。
接下来就是一个函数柯里化的简单应用
function isType(type){
return function(value){
return Object.prototype.toString.call(value) === `[object ${type}]`
}
}
const isArray = isType("Array");
const isString = isType("String");
console.log(isArray([])); //true
console.log(isArray("")); //false
console.log(isString("")); //true
//相当于就是这么写
isType("Array")([])
这样是不是比较高大上呢 !!!
那现在来自己封装一个可以实现柯里化函数的方法
首先分析一下,举个例子,看下面代码
function add(a,b,c,d){
return a+b+c+d
}
假如我们定义了一个函数Curry这个函数可以将普通函数转变成柯里化函数。
正常调用add(1,2,3,4),用柯里化的方式应该是这样的Curry(add)(1)(2)(3)(4)
通过我们自己封装的柯里化函数的方法,将原函数传进去之后就可以化多参为单参,然后依次去调用
那么Curry的返回值肯定是个函数,就像这样
function Curry(){
return function(){
//逻辑
}
}
我们需要将我们的目标函数传入Curry中
function Curry(fn){ // fn表示就是add函数
return function(...args){ // args 表示调用之后传入的参数 就像1,2,3,4
}
}
这样整体逻辑了解了,函数的参数是依次传进来的,我们需要在所有参数传递进来之后再去执行函数并返回结果,那么就需要对参数个数进行判断。这里有一个方法需要了解一下,那就是函数的length属性,比如
function fn(){}
console.log(fn.length) // 0
function fn1(a,b){}
console.log(fn1.length) //2
函数的length属性表示“第一个具有默认值之前的参数个数” 具体可以参考文章 《JS 中函数的 length 属性》
回到分析中,我们需要知道当函数接收的参数个数等于函数的形参个数时,才可以进行执行并返回结果,否则,继续递归进行函数柯里化。
function Curry(fn,arr = []){ // fn表示就是add函数
let len = fn.length // 获取函数fn的形参个数
let argsArr = [] // 用一个数组将fn的参数存起来
return function(...args){ // args 表示调用之后传入的参数 就像1,2,3,4
argsArr = [...arr,...args]
if(argsArr.length < len){ // 当传入参数length小于len时递归柯里化函数
return Curry(fn,argsArr)
}else{ // 当传入参数length等于len时直接执行函数
return fn(...argsArr)
}
}
}
注意点:每次进行递归时,需要将参数携带着,在Curry中接收,然后每次进行拼接。这里大量使用了ES6展开运算符和REST剩余运算符,如果阅读困难,可以查看前文关于展开运算符和剩余运算符。
待续......
写在最后:文中内容大多为自己平时从各种途径学习总结,文中参考文章大多收录在我的个人博客里,欢迎阅览www.tianleilei.cn