Javascript知识零散学习

181 阅读18分钟

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的使用

  1. Promise的状态 Promise的状态表示此时异步执行的状态。Promise一共有三种状态:
  • pending:初始状态
  • fulfilled:操作成功状态
  • rejected:操作失败状态

特点: Promise三个状态不受外界影响,一旦状态改变,就不会再发生变化,Promise对象的状态改变只有两种可能,从pening变为fulfilled和从pending变为rejected.

  1. 基本用法 ES6规定,Promise对象是一个构造函数,用来生成Promise实例。
let promise = new Promise((resolve,reject)=>{
  //...异步代码
  if(/* 异步成功 */){
     resolve(value)
  }else{
    reject(error)
  }
})

Promise构造函数接收一个函数作为参数,该函数有两个参数分别为resolvereject。它们是两个函数,由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对象执行结束了。

  1. 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的针对异步更优雅的解决方案。

用法:

  1. 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
})
  1. 配合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表达式的运算结果。
  1. 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看起来更直观一些吧。

参考: 阮一峰 async 函数的含义和用法 理解 JavaScript 的 async/await

展开(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”去使用。

特点:

  1. 变量必须声明才可使用。
  2. 创设eval作用域,除了全局作用域,函数作用域外,新增eval作用域。
  3. with()被禁用,with语句用于设置代码在特定对象中的作用域。
  4. caller/callee被禁用。
  5. delete使用在var 声明的变量活挂在window的变量上会报错。
  6. delete不可删除属性的对象时会报错。
  7. 对一个对象的只读属性进行赋值会报错。
  8. 对象有重名属性将报错。
  9. 函数有重名的参数会报错。
  10. arguments严格定义为参数,不再与形参绑定。
  11. 函数中的this不再指向window。

参考: 阮一峰 - Javascript 严格模式详解

高阶函数

什么是高阶函数:

  1. 如果一个函数的参数是一个函数(回调函数)
  2. 如果一个函数的返回值是一个函数,当前这个函数也是一个高阶函数

应用场景:扩展业务代码

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") //传参

函数柯里化 例子:判断数据类型

  1. typeof 不能判断对象类型
  2. constructor 判断该数据是由谁构造出来
  3. instanceof 判断谁是谁的实例__proto__
  4. 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