本节思维导图:
什么是闭包? 函数执行形成私有的上下文,当前上下文中的某些东西(如变量等)被外部占用,形成不被释放的上下文,这个就是闭包。通常是大函数内包含小函数,如:
1 2 3 4 5 function outer(i){ return function inner(){ console.log(i) } }
这种类型的好处是,小函数可以公国作用域链机制查找到大函数中的变量i,使用变量i,并且不会对全局变量造成污染,之后我们会通过计数器这个例子来展示闭包的好处。但闭包也有坏处,像这样不被释放的上下文需要占据浏览器内存,比如这里的outer函数就会一直在内存中了。
还有一点人常以为的误区是,并非大函数包含小函数就代表了闭包,只要形成不被释放的上下文,就可以是闭包,比如这样一段代码:
1 2 3 4 5 6 7 8 9 function A (i ) { A = function ( ) { console .log(i) } alert(++i); } A(1 ); A();
这段代码在执行的时候,由于A函数在A执行的上下文中被重构,所以原来的A也不会被释放。
[ 1 ] 例 计数器 > 当前页面有btn按钮和一个显示数字的h3标签
1 2 3 4 5 6 7 8 9 10 11 let num = document.querySelector("#num"); let btn = document.querySelector("#btn"); function outer(count) { return _ => count++; } let counter = outer(1); btn.onclick = function (e) { num.innerHTML = counter(); }
局部代码执行完成之后,形成的私有上下文仍然被外面的代码引用, 没有被浏览器释放,从而起到对变量的 [ 保存/保护 ] 作用。这段代码,每点击一次button,则触发counter函数。outer函数中的count仍然被全局下的couter函数引用,所以该变量不会被释放。
浏览器垃圾回收机制 Chrome浏览器会把当前上下文中,没有被占用的堆内存释放,这就是浏览器的垃圾回收机制。当我们形成闭包的时候,全局上下文中就会占用函数形成私有上下文中的堆内存,函数内部的东西被外部占用,导致函数堆内存不能被浏览器回收,于是函数的私有变量就一直在内存中了。
补充垃圾回收机制相关内容。实现方式:标记清楚和引用计数。
释放内存分为两方面,一个是当前页面的栈内存,一个是当前变量开辟的堆内存。
栈内存的释放 : 加载页面会形成一个全局的上下文,只有页面关闭后,全局上下文才会被释放,函数执行会形成一个私有的上下文,进栈执行,当函数中代码执行完成,大部分情况,形成的上下文都会被出栈释放掉,以此优化栈内存的大小
堆内存的释放 :常用的内存管理机制:关联引用(chrome),引用计数(IE)
关联引用:浏览器在空闲或者指定时间内,查找所有的堆内存,并把没有被任何东西占用的堆内存释放掉
引用计数:创建堆内存,被占用一次,则计数器+1,取消占用则计数器-1,当计数器为0时,内存释放
如果要想内存直接释放可以选择:variable = null;
下面介绍几种闭包的高级应用:
1. 循环处理 我们先来看这样一段代码:
1 2 3 4 5 for (var i = 0 ; i < 5 ; i++) { setTimeout (() => { console .log(i); }, 1000 * i); }
每循环一次,就设置一个定时器,在第i秒后打印。看似没什么问题,但是结果却是将console.log(5) 输出5遍。这是因为SetTimeout执行是异步的,不是循环一次就执行一次,而是遇到SetTimeout之后主线程直接往下走,也就是走到下一个i++,这个过程是同步的。当循环结束的时候,setTimeout中的代码开始执行,控制台输出i,注意:此时循环结束,i为5,所以我们看到的是五次5.
如果我们要输出0~5,则需要把每次循环的i值保存下来,形成一个私有的上下文,这样最后输出的时候,就不调用全局下的i,而是调用每个循环里私有上下文的i。下面介绍三种形成私有上下文的写法,分别为:自执行函数、自定义函数和基于let声明的变量,原理图也如下:
1 2 3 4 5 6 7 8 9 for (var i = 1 ; i < 5 ; i++) { (function (num ) { setTimeout (()=> { console .log(num) },1000 *num) })(i) }
1 2 3 4 5 6 7 8 9 10 11 12 13 let fn = function fn (i ) { return function ( ) { setTimeout (() => { console .log(i); }, 1000 * i) } } for (var i = 1 ; i < 5 ; i++) { let f = fn(i) f() }
1 2 3 4 5 6 for (let i = 0 ; i < 5 ; i++) { setTimeout (() => { console .log(i); }, 1000 * i); }
2. 单例设计模式 基于闭包实现模块化思想 1. 单例设计模式 2. AMD -> request.js 3. CMD -> sea.js 4. CommonJS -> Node本身就是基于这种规范实现的 5. ES6Module 单例模式设计的目的:解决全局变量污染
1 2 3 4 5 6 let name = 'zen' ;let age = 18 ;let name = 'neo' ;let age = 19 ;
1 2 3 4 5 6 7 8 9 10 (function ( ) { let name = 'zen' ; let age = 18 ; })(); (function ( ) { let name = 'neo' ; let age = 19 ; })();
1 2 3 4 5 6 7 8 9 10 11 12 13 (function ( ) { let name = 'zen' ; let age = 18 ; window .hobby = "play" ; })(); (function ( ) { let name = 'neo' ; let age = 19 ; console .log(window .hobby); })();
1 2 3 4 5 6 7 8 9 10 11 let person1 = { name: 'zen' , age: 18 } let person2 = { name: 'neo' , age: 19 }
1 2 1. 高级单例设计模式:JS中最早期的模块化开发思想,实现模块之间的独立性和互通性2. 每个模块之间是独立的,相互之间的信息互不干扰;但是公共方法可以相互调用
单例模式具体实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 let utils = (function ( ) { let isWindow = true , num = 0 ; const queryElement = function queryElement ( ) { }; const formatTime = function formatTime ( ) { }; return { queryElement, formatTime } })(); (function ( ) { let num = 10 ; const checkValue = function checkValue ( ) { utils.formatTime(); } })();
3. 惰性函数 有些操作我们只想要操作一次,比如判断浏览器某个API是否兼容,或者是绑定URL,利用闭包实现的惰性函数给我们提供了这种可能,我们可以让判断的结果或者是用户输入的变量保存起来,下一次就不需要重复执行了。下面我们写一个获取浏览器元素样式的方法,这个方法最好用getComputedStyle
来获取样式,但是getComputedStyle
不兼容IE6~8,我们需要在外层函数先做一次判断,之后的调用就不需要再进行判断了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 function getStyle ( ) { if ('getComputedStyle' in window ) { getCss = function (el, attr ) { return window .getComputedStyle(el)[attr] } } else { getCss = function (el, attr ) { return el.currentStyle[attr] } } return getCss; } var h = document .querySelector("h1" );var getCss = getStyle();var h1_size = getCss(h, 'fontSize' );console .log(h1_size)
html样式:
1 2 3 4 5 6 7 8 9 10 11 /* 文字渐变色 */ h1 { background: -webkit-linear-gradient(-160deg, #FAB74C, #5F3484); -webkit-text-fill-color: transparent; background-clip: text; -webkit-background-clip: text; } <h1 style ="font-size: 22px;" > Lorem ipsum dolor, sit amet consectetur adipisicing elit. Soluta sint quia corporis </h1 >
4. 柯里化函数 currying 又称为部分求值。一个 currying 的函数首先会接受一些参数,接受了这些参数之后,该函数并不会立即求值,而是继续返回另外一个函数,刚才传入的参数在函数形成的闭包中被保存起来。待到函数被真正需要求值的时候,之前传入的所有参数都会被一次性用于求值
(预处理)执行函数形成闭包,把一些变量和值保存起来,以后其下级上下文中如果需要用到这些值,直接基于作用域链查找机制,拿来直接用即可。
面试题 - 解法一
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 function fn ( ) { let outerArr = Array .from(arguments ); return function ( ) { let innerArr = Array .from(arguments ); let arr = outerArr.concat(innerArr); let total = 0 ; arr.forEach((item, index ) => { total += item; }) return total; } } let res = fn(1 , 2 )(3 );console .log(res)
面试题 - 解法二
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 let arr = [10 , 20 , 30 , 40 ];let result = arr.reduce((result, item, index ) => { return result + item; }) let arr = [10 , 20 , 30 , 40 ];let result = arr.reduce((result, item, index ) => { return result + item; }, 0 )
重构reduce
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 function myReduce (arr, callback, initValue ) { let result = initValue; let index = 0 ; if (result === undefined ) { result = arr[0 ]; index = 1 ; } else { result = initValue; } for (; index < arr.length; index++) { result = callback(result, arr[index]); } return result; } let arr = [1 , 2 , 3 , 4 , 5 , 6 ], initValue = 0 ; let result = myReduce(arr, function (result, item ) { return result + item; }, initValue); console .log(result)ES6新方法 const fn = (...outerArgs ) => (...innerArgs ) => outerArgs.concat(innerArgs).reduce((result, item ) => result + item);let arr = [1 , 2 , 3 , 4 , 5 ];let result = fn(1 , 2 )(3 );
5. Compose组合函数 函数式编程中有一个很重要的概念就是函数组合,实际上就是把处理数据的函数像管道一样连接起来,然后让数据穿过管道得到最终结果,例如:
1 2 3 4 5 6 7 8 9 const add1 = (x ) => x + 1 ;const mul3 = (x ) => x * 3 ;const div2 = (x ) => x / 2 ;div2(mul3(div2(add1(mul3(2 )))); let operator = compose(mul3, div2, add1, mul3);console .log(operator(2 ));
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 const add1 = (x ) => x + 1 ;const mul3 = (x ) => x * 3 ;const div2 = (x ) => x / 2 ;function compose (...args ) { return function (initValue ) { let len = args.length; if (len === 0 ) return initValue => initValue; if (len === 1 ) return initValue => args[0 ](initValue); args.reverse() let finalValue = args.reduce(function (result, item ) { return item(result); }, initValue) return finalValue; } } let operator = compose(mul3, div2, add1, mul3);console .log(operator(2 ));
6. 函数的防抖和节流 防抖和节流的应用:例如百度的搜索框:当停止连续输入后就会帮你展示一些联想词,也就用到了防抖:连续触发的一串事件之间是有关联的,并且直到这一串事件连续触发完成才有反馈;比如页面上元素的拖拽,商城秒杀(用户会疯狂点击),都用到了节流:既要保证用户的行为立即有所反馈,又不要事件被过于频繁触发。
debounce.js 创建一个 debounced(防抖动)函数,该函数会从上一次被调用后,延迟 wait 毫秒后调用 func 方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 function debounce (func, waits ) { var timer = null ; return function (...args ) { window .clearTimeout(timer); timer = setTimeout (() => { func(args); timer = null ; }, waits || 300 ) } } function showmsg (...args ) { var msg = null ; args.forEach(item => { item.forEach(innerItem => { typeof innerItem === 'string' ? msg = innerItem : null ; }) }) console .log(`this is msg : ${msg} ` ); } var btn = document .querySelector("#btn" );var f = debounce(showmsg, 1000 )btn.onclick = function (e ) { f(e,'hi' ) }
throttle 创建一个节流函数,在 wait 秒内最多执行 func
一次的函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 function throttle (func, waits ) { var timer = null ; return function (...args ) { if (timer) return ; timer = setTimeout (() => { func(args) timer = null ; }, waits | 300 ) } } function showmsg ( ) { console .log("rolling" ); } let f = throttle(showmsg, 100 )window .onscroll = f;
Reference [1] https://juejin.cn/post/6844903898214301709
[2] https://www.ruanyifeng.com/blog/2009/08/learning_javascript_closures.html
[3] https://www.lodashjs.com/docs/lodash.debounce
[4] https://www.lodashjs.com/docs/lodash.throttle/