闭包及promise
闭包
概述
闭包就是函数嵌套函数,内部函数拥有外部函数的引用,导致这个引用无法被GC回收。
作用域 (上下文对象)
全局作用域
在全局作用域内声明的函数或者变量都归属于当前的全局对象(global对象)
局部作用域(函数作用域)
在函数作用域内声明的变量或者形参都是归属于当前的上下文对象(context 对象)
函数的执行流程
- 预编译
- 调用执行(通过栈地址找到对应的函数执行上下文)
- 执行完毕销毁当前的执行上下文对象
预编译
全局变量的预编译
- 创建对应的GO对象(global object)
- 将形参和变量当作GO的属性 形参和变量赋值 指定默认值undefined
- 形参和实参同步
- 找到函数声明、将对应的值给到函数体
局部变量的预编译
- 创建对应的AO对象(Activation object 执行上下文对象)
- 将形参和变量当作AO的属性 形参和变量赋值 指定默认值undefined
- 形参和实参同步
- 找到函数声明、将对应的值给到函数体
GC (垃圾回收器)
主要回收机制
标记清除法 (通过标记的形式来进行回收 如果当前这个内容没有被使用到就会回收)
引用计数 (通过不断计数操作来进行回收 被使用就+1 未被使用就减1 到0的时候就会被回收)
普通函数自增示例
function fn(){
var a = 1
a++
console.log(a)
}
fn() //2 重新声明对应的a
fn() //2 重新声明对应的a
fn() //2 重新声明对应的a
var a = 1 //属于全局对象的属性 随着window的销毁而销毁
function fn1(){
a++
console.log(a)
}
fn1() //2
fn1() //3
fn1() //4
//从上面的代码可以看出来如果要实现对应的a的自增需要满足的条件为a不会被回收
//又看出来 如果当前的a的值是保存在全局变量那么它也不会被销毁
//从以上俩点可以得到 我们通过返回对应的a的值 用全局变量接收的形式来保证它不会被销毁
function fn2(){
var a = 1
return ++a
}
var v = fn2() //2
var v1 = v//2
//从上述可以看出这个值v是接收对应的a的值不会被回收 但是由于值类型不可变 对应的v和v1是没有关系的 如果将a作为对应的对象的属性 那么我们可以通过引用类型来确定他们的关系
function fn3(){
var a = 1
return {
_a: a,
get a(){
return this._a ++
}
}
}
var v = fn3() //{a:2}
var v1 = v.a //3
var v1 = v.a //4
//通过上述的代码 发现通过返回一个引用数据类型的数据 可以保持对应的数据不会被回收 且对应的数据之间是存在联系的
//通过以上这些 我们可以返回一个函数函数内执行对应的操作 那么就可以反复执行了
function fn4(){
var a = 1
return function(){
return ++a
}
}
var f = fn4()
var v = f() //2
var v1 = f() //3
通过以上讲解发现如果需要让函数内的参数不回收 那么需要保持对应的引用 使它一直存在。通过对应的函数内容返回引用数据类型的形式可以帮助我们保持对应的引用。也就是说通过这种方式就可以使用对应的内容不回收。函数也是一个引用数据类型所以它同样可以保证不被垃圾回收机制回收。
闭包的特点
保持引用,数据进行缓存,不被垃圾回收机制回收。
用途
- 作为缓存 (缓存数据量小、加载快)
- 节流
- 防抖
- 函数柯里化
闭包的优缺点
优点
- 保持了引用 内容不需要重新创建 加载速度快
- 扩大了函数内参数和变量的作用范围
- 利用内部函数来访问外部函数的参数或者变量,避免了数据污染
缺点
- 内存占用大(一直保持引用),滥用容易造成内存泄漏。
- 通过返回函数的形式进行调用 需要不断开辟函数空间 容易内存溢出
防抖(debounce)
概述
在规定时间范围内只执行一次,执行的是最后一次。(防止高频触发 在高频触发的时候只执行一次)
示例
电梯关门时间为10秒钟,A同学先进入电梯点击关门需要等待10s B同学过来了A同学关门操作中断继续等待10s、C同学10s后又过来了B同学的关门操作被中断,最后执行的完整关门操作由C来执行。
代码实现
//传入函数 间隔时间
function debounce(callbackFn,delay){
//准备定时器变量
var timer = null
return function(){
//清除上一次延时器
clearTimeout(timer)
//需要等待 setTimeout 开启新的延时器
timer = setTimeout(()=>{
callbackFn()
},delay)
}
}
节流 (throttle)
在限定时间内执行一次,执行第一次。(减少触发次数)
示例
高铁内上厕所,如果有人亮红灯,没人亮绿灯。A看到亮绿灯进入上厕所灯变为红灯,上完厕所灯变成绿灯。B进入之前就需要查看当前亮什么灯(看厕所是否有人)。如果是红灯就回去,如果是绿灯就进入厕所灯又变成红灯,出来灯又变成绿灯....
代码实现
//传入函数 间隔时间
function throttle(callbackFn,delay){
var timer = null
return function(){
//判断timer是否有值
if(timer) return
//timer没有值 延时器开启
timer = setTimeout(()=>{
callbackFn()
//执行完成清除延时器
clearTimeout(timer)
//将当前的timer值设置为null
timer = null
},delay)
}
}
函数柯里化(currying)
将多参数的函数分解成单一参数的函数,主要是可以延迟传入参数。
函数柯里化优点
- 延迟传入参数
- 参数复用
函数柯里化核心
参数个数没到返回函数,参数个数到返回结果
简单函数柯里化
//基础求和函数
function sum(a, b, c) {
return a + b + c
}
console.log(sum(1, 2, 3)) //6
//简单函数柯里化
function sumCurrying(a) {
return function (b) {
return function (c) {
return a + b + c
}
}
}
console.log(sumCurrying(1)) //返回函数
console.log(sumCurrying(1)(2)) //返回函数
console.log(sumCurrying(1)(2)(3)) //6
//复用参数 延迟传入参数
let fn = sumCurrying(1)(2) //这个里面的参数1、2被复用了
console.log(fn(3))//6
console.log(fn(4))//7
console.log(fn(5))//8
高阶函数柯里化
//高阶函数柯里化 (利用高阶函数的形式实现柯里化操作)
//传入一个函数 及对应的参数
// 参数个数没到返回函数,参数个数到返回结果
function currying(fn,...arg){
return function(...args){
//获取所有的参数组成的数组
let arr = args.concat(arg)
//判断当前是否满足函数需要的参数个数
if(fn.length === arr.length){ //满足个数 返回对应的结果
return fn.apply(this,arr)
}else{//不满足个数 继续返回函数
return currying.call(this,fn,...arr)
}
}
}
//调用柯里化函数
var curryingFn = currying(sum,1)
console.log(curryingFn(2,3))//6
console.log(curryingFn(2)(3))//6
console.log(curryingFn(2)()()()()()()()()()()()()()()(3))//6
console.log(curryingFn()()()()()()()()(2)()()()()()()(3))//6
console.log(curryingFn())
console.log(curryingFn(2))
console.log(curryingFn(2)(3))
bind 也是一个柯里化函数
function say(a,b){
console.log(this,a,b)
}
say.bind({},'1')(2)
反柯里化
将一个原本对象没有的方法进行调用 (利用的this指向更改)
简单反柯里化
//反柯里化操作 鸭子类型(长的像的类型替代为这个内容)
var str = '123456'
let newString = Array.prototype.reduce.call(str,(prev,v)=>{
return prev + v+';'
})
console.log(newString)
反柯里化封装
Function.prototype.unCurrying = function(){
var self = this;
return function(){
//执行方法 call方法 使用self执行
//retrun self.call(arguments)
return Function.prototype.call.apply(self, arguments);
}
}
//使用toString.call检索类型
console.log( Object.prototype.toString.call({})) //[object Object]
console.log( Object.prototype.toString.call([]))//[object Array]
console.log( Object.prototype.toString.call(new Function()))//[object Function]
console.log( Object.prototype.toString.call(new RegExp()))//[object RegExp]
//使用反柯里化进行类型检索
let check = Object.prototype.toString.unCurrying()
console.log(check([]))
console.log(check({}))
console.log(check(new Function))
Promise
概述
promise是es6新增的一个类,主要用于处理异步问题(异步代码同步执行)。主要解决了回调地狱的问题。它翻译为承诺,它有三种状态。
promise的三种状态
promise状态是唯一的(一旦成功或者失败将不能继续更改)
- 成功状态 (fulfilled)
- 拒绝状态 (rejected)
- 等待状态 (pending)(默认状态)
promise类的实例化
使用new关键词进行实例化
//传入一个对应的函数 一般传入这个函数内的代码是异步
//这个传入的函数里面有俩个参数 这个俩个参数分别是俩个函数
//第一个函数是更改成功状态的方法 第二个函数是更改拒绝状态的方法
let promise = new Promise((resolve, reject) => {
setTimeout(() => {
console.log(1)
// resolve('你好') //调用更改为成功状态的方法 传入对应的参数这个参数就返回的结果
// reject('世界') //调用更改状态为失败的方法 传入对应的参数也是返回的结果 报错
})
})
console.log(promise) //没有调用更改状态的方法 它的结果为等待状态
promise的相关原型方法
then 处理成功的方法
// then方法 处理成功状态的方法
new Promise((resolve, reject) => {
//更改状态为成功 传入对应的结果值
resolve('成功了娶老婆!!')
}).then((res) => { //then里面的第一个函数内容的第一个参数可以接收对应的resolve方法传递的结果
console.log('发财了!!', res)
}) //传入一个函数这个函数就是用于成功状态下的处理函数
//传入俩个函数
// then方法 处理成功状态的方法
new Promise((resolve, reject) => {
//更改状态为成功 传入对应的结果值
// resolve('成功了娶老婆!!') promise的状态值是唯一的 只能是一种状态
// 更改状态为失败
reject('回家躲着')
}).then((res) => { //then里面的第一个函数内容的第一个参数可以接收对应的resolve方法传递的结果
console.log('发财了!!', res)
},(error)=>{
console.log('失败了',error)
}) //传入俩个函数在then方法中 第一个函数是用于处理成功状态的函数 第二个函数是用于处理失败状态下的函数
//then方法如果传递不是一个函数 就会发生值穿透
// then方法 处理成功状态的方法
new Promise((resolve, reject) => {
//更改状态为成功 传入对应的结果值
resolve('成功了娶老婆!!')
}).then(1) //传递不是一个函数 就会发生值穿透
.then(2).then(3)
.then((res)=>{
console.log(res)
}) //因为then返回的是一个promise对象 所以可以无限点下去
//then里面如果return数据 那么相当于调用下一个对应的resolve方法 传递给下一个then(成功接收)
new Promise((resolve, reject) => {
//更改状态为成功 传入对应的结果值
resolve('成功了娶老婆!!')
}).then(()=>{
return 'hello 世界'
}).then(Math.pow).then(1).then((res)=>{
console.log(res) //NaN 接收Math.pow('hello 世界')
})
//then里面如果抛出错误 那么相当于调用下一个对应的reject方法 传递给下一个then(错误接收)
new Promise((resolve, reject) => {
//更改状态为成功 传入对应的结果值
resolve('成功了娶老婆!!')
}).then(()=>{
throw new Error()
}).then(()=>{},()=>{
console.log('接收处理')
})
catch 处理失败状态的方法
//catch方法 处理失败的状态 传递一个处理错误的函数 catch中return 进入下一个then 抛出错误进行下一个catch
//它也会发生值穿透
new Promise((resolve,reject)=>{
reject('错误了')
}).catch((error)=>{
//console.log(error)
throw new Error()
}).catch((error)=>{
console.log(error,'下一次错误')
})
new Promise((resolve,reject)=>{
reject('错误了')
}).catch((error)=>{
throw new Error()
}).catch(1).catch(2).catch(err=>{
console.log(err)
})
finally 处理状态更改的方法
new Promise((resolve,reject)=>{
// reject('错误了')
resolve()
}).finally(()=>{
console.log('处理了')
}) //无法接收传递的数据
总结
- promise的所有的方法返回的都是一个新的promise对象
- then方法允许传入俩个参数(参数类型为函数)它的第一个函数参数接收resolve的处理 第二个函数参数接收reject的处理
- catch接收对应的reject的处理,抛出错误它也可以接收(抛出错误状态会更改为rejected)
- then和catch都会发生值穿透问题 (传入参数非函数就会发生值穿透)
- then和catch中如果return数据那么就会将数据传递给下一个then 如果抛出错误就会将数据传递给下一个catch
- then里面的第一个函数可以接收resolve调用传递的结果,第二个函数可以接收reject传递的结果 catch也可以接收reject传递的结果
- finally 处理状态更改的方法(只要状态发生变化它就会被调用)
promise的静态方法
- all 同步并行执行所有的promise
- race 竞速 返回最快执行完的promise
- reject 返回拒绝状态的promise
- resolve 返回成功状态的promise
- allsettled 同步非并行执行所有的promise
// resolve 返回一个成功状态promise
console.log( Promise.resolve('hello'))
// reject返回一个失败状态的promise
console.log(Promise.reject('失败'))
//race 竞速 返回最先执行完的promise结果
var promise1 = Promise.resolve('a')
var promise2 = Promise.resolve('b')
var promise3 = Promise.reject('c')
var promise4 = Promise.resolve('d')
//传入一个promise数组
let result = Promise.race([promise2,promise3,promise1,promise4])
console.log(result)
//all 同步并行执行所有的promise 当一个错误返回错误的那个promise的结果 当成功返回所有的结果
let promiseResult = Promise.all([promise2,promise3,promise1,promise4])
console.log(promiseResult)
//allsettled
// 同步非并行执行所有的结果 不管是否成功失败都返回所有的结果 且状态为成功
let promiseResult1 = Promise.allSettled([promise2,promise3,promise1,promise4])
console.log(promiseResult1)
回调地狱
回调函数的无限嵌套 造成了回调地狱
- 回调地狱主要是代码的可读性低
- 可维护性低 造成代码没有存在的价值
//异步代码同步执行的方法 主要是通过回调函数来解决对应的异步代码同步执行的问题
function fn(arg, callback) {
setTimeout(()=>{
console.log(arg)
callback()
})
}
//回调地狱
fn(1, () => {
fn(2, () => {
fn(3, () => {
fn(4, () => {
fn(5, () => {
fn(6, () => {
fn(7, () => {
// ....
})
})
})
})
})
})
})
利用promise来解决回调地狱
利用then方法返回的是一个promise的特性 (无限调用then方法)
利用then方法中调用return可以进入下一个then的特性
核心实现
在对应的then方法调用return 返回一个新的promise来解决回调地狱的问题
new Promise((resolve,reject)=>{
setTimeout(()=>{
console.log(1)
//调用resolve进入then
resolve()
})
}).then(()=>{
//返回一个新的promise 进行下一个then
return new Promise((resolve,reject)=>{
setTimeout(()=>{
console.log(2)
//调用resolve执行下一个then的内容
resolve()
})
})
}).then(()=>{
return new Promise((resolve,reject)=>{
setTimeout(()=>{
console.log(3)
resolve()
})
})
}).then(()=>{
console.log(4)
})
ES7新增的async awiat
相关说明
- async和awiat是一个语法糖
- async 是用于修饰函数的(async修饰的函数执行返回一个新的promise)
- awiat 是用于修饰promise的 (只能在async修饰的函数内使用)
示例
async function fn() {
// return undefined 默认返回的
// throw new Error()
}
//async修饰的函数执行返回一个新的promise
//在async修饰的函数内返回一个内容 相当于调用resolve方法 会让当前的promise变成成功状态
//抛出错误相当于调用的reject 传递对应的错误
console.log(fn()) //promise 成功状态接收的值为undefined
//await 会等待当前内容执行
// await一定在async里面
async function say() {
//遇到await会跳出当前的这个promise
await new Promise((resolve, reject) => {
setTimeout(() => {
console.log(1)
resolve()
})
})
await new Promise((resolve, reject) => {
setTimeout(() => {
console.log(4)
resolve()
})
})
console.log(2)
}
//同步优先执行 如果没有await 那么对应的同步先打印 异步后打印 结果为 2 1
//await是在等待当前状态发生变化 变化了才进行下一步 给整个函数的promise为等待状态
say()
console.log(3)
eventLoop(事件轮询)
JavaScript是单线程(js引擎只有一个),同步先执行异步后执行。
异步执行机制
异步代码执行机制其实就由webApi进行任务队列(先进先出)的分发。
任务队列的组成又分为宏任务队列和微任务队列
- 宏任务队列 (属于基础的异步代码 JavaScript 、事件执行、定时器、延时器....)
- 微任务队列 ( 属于宏任务里面的promise 就是微任务)
任务队列的执行机制被称为事件轮询
注意事项
- 先宏后微
- 先进入JavaScript的宏任务 然后推入里面的同步代码 再去找对应的微任务队列。
- 将对应的微任务队列的内容按照加入队列的顺序进行推入执行栈(加载顺序),执行完对应所有的微任务开启下一个宏任务。
- 如果当前宏任务是setTimeout或者是setInterval那么这个执行的机制取决于间隔的时间(间隔时间一直取决于加载顺序)
- 将当前的宏任务 里面的同步代码推入执行栈 再找到对应微任务队列执行微任务...不断重复直到微任务队列及宏任务队列都清空。