一、闭包的概念:
闭包不是具体的代码,而是函数运行的一种机制
函数执行会形成一个私有的上下文,如果上下文中的某些内容(一般指的是堆内存地址)被上下文以外的一些事务(例如:变量/事件绑定等)所占用,则当前上下文不能被出栈释放[浏览器的垃圾回收机制GC所决定的] => "闭包"的机制:形成一个不被释放的上下文
函数执行形成一个私有的上下文,此上下文中有私有变量,和上下文中的变量互不干扰;也就是当前上下文把这些变量保护起来了,我们把函数的这种保护机制称之为闭包。
市面上很多人认为,形成的私有上下文很容易被释放,这种保护机制存在时间太短了,不是严谨意义上的闭包,他们认为只有形成的上下文不被释放,才是闭包,而此时不仅保护了私有变量,而且这些变量和存储的值也不会被释放掉,保存起来了
- 保护:保护自己的私有变量不被外界干扰
- 保存:上下文不被释放,上下文中的私有变量及值都会被保存起来,可以供其下级上下文中使用 利用这两种机制,可以实现高阶编程技巧
函数套函数,子函数使用父函数的参数或者变量
并且子函数被外界所引用,此时父级形成闭包环境
父级的参数或者变量不被浏览器垃圾回收机制回收.
此时,打印父函数的返回值,有个属性为Scopes
Scopes下有个closure的属性,closure 就是闭包。
使用闭包可以一直存储父级的参数或者变量
不被外界的函数或者变量所干扰(污染)
function fn() {
let a = 2;
let obj = {
a: function fn2() {
console.log(a);
},
b: function fn3() {
console.log(a);
}
};
return obj;
};
let f = fn();
console.dir(f);
二、闭包的弊端
- 如果大量使用闭包,会导致栈内存太大,页面渲染变慢,性能受到影响,所以项目中需要合理使用闭包
- 某些代码会导致栈溢出挥着内存泄漏,这些操作都是需要注意的
// 递归:函数执行中再次调用自己
// 下面案例是死递归,Uncaught RangeError: Maximum call stack size exceeded 即内存溢出
function fn(x){
fn(x+1);
}
fn(1);
- 如果在项目中用递归,一定要有一个结束的条件,否则会导致内存溢出
三、利用闭包机制实现高阶编程技巧
- 模块化思想
- 惰性函数
- 柯理化函数
- react高阶组件
- 函数的防抖和节流
- bind
- compose组合函数
3.1 模块化思想
单例 -> AMD(require.js) -> CMD(sea.js) -> CommonJS(NODE) -> ES6Moudle
- 没有模块化思想之前,团队协作开发或者代码量较多的情况,会导致全局变量污染(全局变量冲突),为了避免这种情况,就需使用闭包形成私有作用域,避免影响全局上下文。
// 没有模块化之前,不同的板块会由不同的人员来开发,定义的变量可能会相同,这样很容易造成全局变量污染(下面的变量覆盖上面的变量)
// 实现天气板块
var time = "2021-10-13";
function getQuery() { "..." }
function changeCity() { "..." }
// 实现咨询板块
var time = "2021-10-13";
function getQuery() { "..." }
function changeCity() { "..." }
// 为了避免变量全局污染,会把不同板块的代码放在一个自执行函数中,形成闭包的保护作用,防止了全局变量污染
// 但是这样产生的问题是:每个版块的代码都是私有的,不能相互调用
(function () {
// 实现天气板块
var time = "2021-10-13";
function getQuery() { "..." }
function changeCity() { "..." }
})();
(function () {
// 实现咨询板块
var time = "2021-10-13";
function getQuery() { "..." }
function changeCity() { "..." }
})();
// 针对上面的问题,想要调用别人的板块的方法
// 但是这样会出现的问题是:在全局不能挂载太多方法,挂载多了,还是会引发全局变量的污染
(function () {
// 实现天气板块
var time = "2021-10-13";
function getData() { "..." }
function changeCity() { "..." }
// 把需要供别人调用的API方法挂载到全局
window.getData = getData;
})();
(function () {
// 实现咨询板块
var time = "2021-10-13";
function changeCity() { "..." }
getData();
})();
- 针对上面的问题,想到利用对象
- 对象的特点: 每一个对象都是一个单独的堆内存空间(单独的实例->Object),这样即使多个对象中的成员名字相同,也互不影响
- 仿照其他后台语言,其实obj1/obj2不仅仅称为对象名,更被称为[命名空间](给堆内存空间起一个名字)
- 每一个对象都是一个单独的实例,用来管理自己的私有信息,即使名字相同,也互不影响,其实这就是“JS中的单例设计模式”
var obj1 = {
a: 1,
b: 2,
query() { "..." }
}
var obj2 = {
a: 1,
b: 2,
query() { "..." }
}
return一个对象,把需要供别人调用的方法暴露出来,并用一个变量来接收返回值,这样其他模块就可以调用了
// 高级单例设计模式 -> 闭包+单例的结合(早期JS模块化思想)
var weatherMoudle = (function () {
// 实现天气板块
var time = "2021-10-13";
function getData() { "..." }
function changeCity() { "..." }
return {
getData
}
})();
var infoMoudle = (function () {
// 实现咨询板块
var time = "2021-10-13";
function changeCity() { "..." }
weatherMoudle.getData()
})();
3.2 惰性函数
- 举个例子:比如要获取页面中某个元素的样式,我们通常会用getComputedStyle()方法
window.getComputedStyle(元素):获取当前元素经过浏览器计算的样式(返回样式对象)- 但这个方法有兼容性问题:在IE6~8中,不兼容这种写法,需要使用
“元素.currentStyle”来获取 - “属性 in 对象”: 检测当前对象是否有这个属性,有则返回true,没有则返回false
于是我们可以写这样一个方法
function getCss(element, attr) {
if ('getComputedStyle' in window) {
return window.getComputedStyle(element)[attr]
}
return element.currentStyle[attr];
}
var body = document.body;
console.log(getCss(body, 'height'));
console.log(getCss(body, 'margin'));
console.log(getCss(body, 'background'));
但这样的方法并不好,每次调用getCss方法都要判断是否有这个属性,即使浏览器并没有改变,同样要判断
于是我们可以进行优化:第一次执行的getCss我们已经知晓是否兼容了,第二次及以后再次执行getCss,则不想在处理兼容的校验了,其实这种思想就是“” 惰性思想(懒,干一次搞定的,绝对不去干第二次)
// 把校验的过程 即'getComputedStyle' in window 存起来,这样每次校验的时候校验的是flag的值,少了查找的过程
var flag = 'getComputedStyle' in window
function getCss(element, attr) {
if (flag) {
return window.getComputedStyle(element)[attr]
}
return element.currentStyle[attr];
}
var body = document.body;
console.log(getCss(body, 'height'));
console.log(getCss(body, 'margin'));
console.log(getCss(body, 'background'));
但这样其实不算严谨意义上的惰性思想
function getCss(element, attr) {
// 第一次执行,根据函数是否兼容,进行函数的重构
if ('getComputedStyle' in window) {
getCss = function getCss(element, attr) {
return window.getComputedStyle(element)[attr];
}
} else {
getCss = function getCss(element, attr) {
return element.currentStyle[attr];
}
}
// 为了保证第一次也可以获取信息,则需要把重构的函数执行一次
return getCss(element, attr)
}
var body = document.body;
console.log(getCss(body, 'height'));
console.log(getCss(body, 'margin'));
console.log(getCss(body, 'background'));
getCss(body, 'height')函数第一次执行,正常校验,然后走第一个小函数,执行第一个小函数,拿到结果
第二次再执行getCss(body, 'margin'),不会再执行大函数内容了,会直接执行小函数,即不会走判断,第三次也一样
这就是标准的“惰性思想”
第一次执行getCss,形成一个私有上下文,在这个私有上下文下,把其中的某个堆(小函数getCss)又重新赋值给全局下的getCss,此时,第一次执行上下文不释放,形成闭包;之后再次执行getCss其实执行的就是第一次没被释放的闭包,保证第二次和第三次都执行的是小函数getCss。
3.3 柯理化函数
函数柯理化:预先处理的思想(形成一个不被释放的闭包,把一些信息存储起来,以后基于作用域链,访问到事先存储的信息,然后进行相关的处理,所有符合这种模式(或者闭包应用的)都被称为柯理化函数。
举例:写出一个方法,实现第一次执行sum(20) -> 输出10+20,第二次执行sum(20, 30) -> 输出10+20+30,以此类推
// x为预先存储的值
function curring(x) {
// ?
}
var sum = curring(10);
console.log(sum(20)); // 10+20
console.log(sum(20, 30)); // 10+20+30
首先需要在curring函数中写一个返回值,用sum来接收
因为返回的函数参数可能是一个或多个,如何能获取到不确定个数的参数信息呢?
- 参数可以用
...args:基于ES6的剩余运算符获取传递的实参信息 -> 数组
// x为预先存储的值
function curring(...args) {
// ?
}
var sum = curring(10);
console.log(sum(20)); // 10+20
console.log(sum(20, 30)); // 10+20+30
- 用
arguments:arguments是个内置的实参集合,它返回的是类数组;需要把类数组转化为数组- var args = Array.from(arguments);
- var args = [].slice.call(arguments);
function curring(x) {
return function (...args) {
args.unshift(x); // unshift() 在数组开头添加一项
// 数组求和方案一
var total = 0;
for (let i = 0; i < args.length; i++) {
total += args[i]
}
return total
}
}
var sum = curring(10)
console.log(sum(20));
console.log(sum(20, 30));
function curring(x) {
return function (...args) {
args.unshift(x); // unshift() 在数组开头添加一项
// 数组求和方案二
var total = 0;
args.forEach((item) => {
total += item;
})
return total
}
}
var sum = curring(10)
console.log(sum(20));
console.log(sum(20, 30));
以上两种数组求和的方法都是用了循环,不同的是上面for循环运用了命令式编程,下面forEach循环运用了函数式编程
- 命令式编程:自己编写代码,管控运行的步骤和逻辑(自己灵活掌控执行步骤)
- 函数式编程:具体实现的步骤已经被封装称为方法,我们只需要调用方法获取结果即可,无需关注怎么实现的(用起来方便,代码量减少),但弊端是自己无法灵活掌控执行步骤(项目中大部分用函数式编程)
// 数组求和方案三
// ary.join() 用指定的分隔符把数组各项进行分隔并转变为字符串
// eval() 把字符串变成JS表达式
var total = eval(args.join('+'));
// 数组求和方案四
function curring(x) {
return function (...args) {
args.unshift(x);
// 数组求和
return args.reduce((result, item) => result + item)
}
}
var sum = curring(10)
console.log(sum(20)); // 10+20
console.log(sum(20, 30)); // 10+20+30
3.4 compose组合函数
- 在函数式编程中有一个很重要的概念就是函数组合,实际上就是把处理数据的函数像管道一样连接起来,然后让数据穿过管道得到最终的结果。例如:
const add1 = (x) => x + 1;
const mul3 = (x) => x + 3;
const div2 = (x) => x / 2;
div2(mul(add1(0))); // 3
而这样的写法可读性太差了,我们可以构建一个compose函数,它接受多个函数作为参数(这些函数都只接受一个参数),然后compose返回的也是一个函数,达到以下的效果:
const add1 = (x) => x + 1;
const mul3 = (x) => x + 3;
const div2 = (x) => x / 2;
// div2(mul(add1(add1(0)))); // 3
const operate = compose(div2, mul3, add1)
operate(0) //=> 相当于div2(mul3(add1(0)))
operate(2) //=> 相当于div2(mul3(add1(2)))
简而言之,compose可以把类似于f(g(h(x))) 这种写法简化成 compose(f, g, h)(x) 那么compose函数的编写为
const add1 = (x) => x + 1;
const mul3 = (x) => x + 3;
const div2 = (x) => x / 2;
// funcs存储的是最后需要执行的函数及其顺序(最后传递的函数优先执行)
// + 执行compose只是把最后要执行的函数及顺序事先存储起来,函数还没有执行(柯理化思想)
// + 返回一个operate处理函数,执行operate,并且传递初始值,才按照之前存储的函数及顺序依次执行函数
function compose(...funcs) {
// funcs -> [div2, mul3, add1]
return function operate(x) {
// x->0 初始值
// 这里需判断一下compose如果没传值的情况
if (funcs.length === 0) return x
// 此时我们需要先把add1执行,把初始值x传给add1,把add1执行的结果传给mul3,执行mul3,再把mul3执行的结果传给div2,最后执行div2 => 也就是依次遍历数组中的每一项,把上一次处理后的结果传给下一次,可以想到数组的reduce方法
// 如果只传了一个函数,则没有必要用reduce方法
if (funcs.length === 1) return typeof item !== "function" ? funcs[0](x) : x;
return funcs.reduceRight((result, item) => {
// 这里可以先做个判断,判断item是否为韩束,不是函数则跳过,执行下一个函数
if (typeof item !== "function") return result
return item(result)
}, x)
}
}
const operate = compose(div2, mul3, add1)
operate(0) //=> 相当于div2(mul3(add1(add1(0))))
operate(2) //=> 相当于div2(mul3(add1(add1(2))))
3.5 函数的防抖(debounce)和节流(throttle)
在高频触发场景下,需要进行防抖和节流
- 狂点一个按钮
- 页面滚动
- 输入模糊匹配
- 输入框金额后实时试算利息等
我么自己设定,多长时间内,触两次及以上就算“高频”:封装方法时候需要指定这个频率(可以设置默认值)
防抖
在某一次高频触发下,我们只识别一次(可以控制开始触发,还是最后一次触发)
详细:假设我么规定500ms触发多次算是高频,只要我们检测到是高频触发了,则在本次频繁操作下(哪怕你操作了10min),也只触发一次
// 点击按钮
submit.onClick = function () {
console.log('ok')
}
节流
在某一高频触发下,我们不是只识别一次,按照我们设定的间隔时间(自己规定的频率),没到这个频率都会触发一次
详细:假设我们规定频率是500ms,我们操作了10min,触发的次数=(10601000)/500
// 页面滚动
window.onscroll = function () {
// 默认情况下,页面滚动中:浏览器在最快的反应时间内(4-6ms),就会识别监听一次事件触发,把绑定的方法执行,这样导致方法执行的次数过多,造成不必要的资源浪费
console.log('ok')
}
3.5.1 业务场景中处理防抖
// 业务场景中的处理技巧1:标识处理
let flag = false;
submit.onclick = function () {
if (flag) return;
flag = true;
setTimeout(() => {
// 事件处理完
flag = false;
}, 1000)
}
// 业务场景中的处理技巧2:按钮重置为灰色,移除事件绑定
let flag = false;
function handle() {
submit.onclick = null;
submit.disabled = true;
// ...
setTimeout(() => {
submit.onclick = handle;
submit.disabled = false;
}, 1000)
}
submit.onclick = handle;
但以上处理都不算严谨意义上的防抖,我们可以封装一个debounce方法
<button id="submit">点击</button>
/*
* func:具体要处理的业务函数
* wait: 规定多长时间算高频触发
* immediate:是否是在最开始触发
**/
function debounce(func, wait, immediate) {
// 多个参数及传递默认的处理
if (typeof func !== "function") throw new TypeError("func must be an function!");
if (typeof wait === "undefined") wait = 500;
if (typeof wait === "boolean") {
immediate = wait;
wait = 500;
}
if (typeof immediate !== "boolean") immediate = false;
console.log(wait);
// 设定定时器返回值标识
// 定时器目的:检测500ms内是否会触发第二次,如果有,则为高频触发;
// 第一次proxy执行,设置一个定时器,等4ms后,第二次proxy执行,清除之前设定的定时器,重新设定,再去检测500ms内是否有第二次触发
// 比如按钮疯狂点击了100次,之前99次定时器都清除了,只留最后一个,等过了500ms,发现没有触发第二次,则执行handle函数,所以这个handle函数最终只执行一次
let timer = null;
return function proxy(...params) {
// 我们无法控制浏览器多长时间触发一次,所以把要处理的逻辑放到代理函数中,用代理函数proxy来控制handle
let _this = this;
let now = immediate && !timer;
clearTimeout(timer);
timer = setTimeout(() => {
timer = null;
!immediate ? func.call(_this, ...params) : null;
}, wait);
// 第一次触发就立即执行
now ? func.call(_this, ...params) : null;
}
}
function handle(ev) {
// 具体点击时处理的业务
// 弹窗、跳转页面、调接口等
console.log('ok', this, ev);
}
submit.onclick = debounce(handle, 500, true)
// submit.onclick = proxy; // 疯狂点击的情况下,proxy会被疯狂执行,我们需要在proxy中根据频率管控handle的执行次数
// submit.onclick = handle; // 如果这样写,浏览器每过4-6ms就要执行一次 handle -> this:submit 传递一个事件对象
柯理化思想:
执行debounce函数,只是把未来需要执行业务逻辑的函数func,和需要管控的频率wait,和控制在哪个阶段触发的标识immediate这三个参数预先存起来,然后把返回的函数赋值给onclick,此时debounce执行->形成闭包;
点击onclick,实际上就是执行proxy方法
3.5.2 业务场景中处理节流
/*
* func:具体要处理的业务函数
* wait: 规定多长时间触发一次
**/
function throttle(func, wait) {
// 多个参数及传递默认的处理
if (typeof func !== "function") throw new TypeError("func must be an function!");
if (typeof wait === "undefined") wait = 500;
let timer = null;
let privious = 0; //记录上一次触发(操作)的时间
return function proxy(...params) {
let _this = this;
let now = new Date(); // 记录当前这次触发操作的时间
let remaining = wait - (now - privious); // 拿两次时间间隔差和wait进行比较
if (remaining <= 0) {
// 临界点的处理
clearTimeout(timer);
timer = null;
// 两次间隔时间超过wait了,直接执行即可
privious = now;
func.call(_this, ...params);
} else if (!timer) {
// 之前没有设置过定时器,再设置定时器
// 两次触发的间隔时间没有超过wait,则设置定时器,让其等待remaining这么久之后执行一次
timer = setTimeout(() => {
clearTimeout(timer);
timer = null;
privious = new Date(); // 此时不能用now赋值,因为中间有时间消耗
func.call(_this, ...params);
}, remaining)
}
}
}
function handle() {
console.log('ok');
}
window.onscroll = throttle(handle, 1000); // 相当于执行 window.onscroll = proxy