【闭包】closure(看完不会你砍我!!!)
前言:
闭包是js中最强大的特性,也是js相较于其他语言最令人着迷的地方,如果你对它研究的透彻,你会为它着迷,否则你会被吓住。
请仔细阅读文中的判断句,如果对某句话不理解可以留言,我会回复的,或者一起讨论怎么描述更为准确。
闭包的前置知识点:
1、在函数中如果不使用var定义变量,那么js引擎会自动将该变量添加为全局变量。这个叫做js的变量的声明前置
2、全局变量在声明的那刻起就一直在内存中,除非关闭这个页面
let number = 0 function a() { console.log(++number) } a() // 1 a() // 2
3、局部变量在函数运行完后销毁,下一次调用该函数再重新创建该局部变量
function a() { let number = 0 console.log(++number) } a() // 1 a() // 1
4、函数内部可以使用局部变量,也可以使用全局变量,也可以使用它的父级函数的局部变量。函数外不可以使用某个函数的局部变量(第3点,a函数外是不能访问到number变量的)
5、垃圾回收机制:每隔一段时间,垃圾回收器去内存中找到那些不再使用的值,然后给它释放掉,一次来缓解内存的压力。如果一个函数被【全局】变量引用(将函数赋值给该变量)了,那么它将不会被垃圾回收机制回收,这种情况多了就会造成内存拥堵,严重时会造成【内存泄漏】
6、词法环境(词法作用域):根据变量声明的位置确定该变量在何处可用(重点)----也可以说,当一个函数执行时和声明时的词法作用域不是同一个,闭包就产生了
function test(fn) { const a = 1 fn() } const a = 2 function fn() { console.log(a) } test(fn) // 2 为什么是2不是1?取决于函数声明时用的是哪个a
多提一句:this的值是在函数执行时决定,而不是函数定义时决定,它俩正好相反。this就是谁调用了我,我就指向谁
闭包解决了什么问题:
js有回收机制,如果一个函数没有被引用,该函数执行完后它的作用域就会被销毁;如果该函数被引用了,它执行完后作用域将不会被销毁。
闭包的定义:
MDN:一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)。
【在JavaScript中,每当创建一个函数,闭包就会在函数创建的同时被创建出来】,也就是说,所有的函数,都是闭包。整个浏览器都是一个作用域,其中的每个函数(如fn)都是作为window的一个属性,window对象为父函数,fn就是子函数,调用fn时通常前面的【window.】不写,这就是一个函数嵌套函数的关系。所有的函数都有父级,所以都是子函数,所以函数都是闭包。
JavaScript高级程序设计第三版:闭包是指有权访问另一个函数作用域中的变量的函数。(子函数就是一个闭包)
常见的方式:函数内部创建一个函数
JavaScript高级程序设计第四版:闭包指的是那些引用了另一个函数作用域中变量的函数,通常是在嵌套函数中实现的。(子函数中引用了父函数的变量,那么子函数就是一个闭包)
形成闭包的条件:
1、函数嵌套函数
2、内部函数引用了外部函数的局部变量/局部函数【也就是说,如果b函数中没有使用a函数中的变量,那么在b执行的过程中不会产生闭包】
注意:旧版谷歌浏览器中即使不调用内部函数也会产生闭包,新版谷歌不调用内部函数时不会产生闭包。
b函数在执行时产生了闭包,b函数就是一个闭包,但是这个闭包没有被保持下来,在b执行完后闭包就已经没了。
如果说闭包没有被保持下来,那么闭包的作用就得不到体现。
例子,判断b函数中有没有形成闭包:
function a() { var num = 100 var s = 'xxx' function b() { console.log(b) // 使用了a函数中的局部函数,此时会形成闭包 { b: fun } } b() } a() /* 预编译过程: 当a调用时,产生a的AO对象 aAO: { num: 100, str: 'xxx', b: fun } 当b调用时,产生b的AO对象 bAO: {} b里面没有可预编译的东西 代码执行到b函数中,此时的[[Scopes]]为: 0: bAO 1: aAO 2: GO 打印的b在b函数中没有,向上查找,在a中找到局部函数b,即打印b的function */
闭包的保持:
如果希望在函数调用后,闭包依然保持,就需要将内部函数返回到外部函数的外部
/* 闭包的保持:将内部函数返回到外部函数的外面 */ function a() { var num = 100 return function() { console.log(num++) // 使用了外部函数的num,这里形成闭包 } } var b = a() console.dir(b) b() // 结果:100 执行b产生bAO b函数中使用了a函数中的num,这个num就被保存在内存中,不被垃圾回收机制回收 b() // 结果:101 执行b产生另一个bAO
什么时候需要使用闭包:
一般来说,函数外是不能访问函数内的变量的,闭包就是来解决这个问题
想让一个变量长期存储在内存中,以便将来使用,但是不想定义全局变量,以免该变量易受到污染,就要想到使用闭包
闭包的应用:
1、在函数外部访问私有变量
/* 闭包的应用:函数外部可以访问函数内部的变量 */ function a() { var num = 100 return function() { return num++ } } var b = a() var num = b() var num1 = b() console.log(num) // 100 console.log(num1) // 101
2、实现封装,私有属性和私有方法
3、防止污染全局变量
4、回调函数的本质是利用了闭包的特性,将局部变量缓存起来了
……
闭包的实际应用
闭包的作用:
①闭包可以使私有变量不被垃圾回收机制回收,这样,当我们需要使一个变量长期存储在内存中,就可以使用闭包代替全局变量
②
闭包的缺点:
函数中用var定义的变量在该函数运行完即被销毁。而在闭包中,内层函数调用了外层函数的局部变量,并且返回给外面的全局变量,该局部变量会被存储起来。因为外层函数返回的是一个函数(返回了内层函数),函数就是一个对象,所以该局部变量被保存到了堆中,即使将接收的那个全局变量设置为null,也不会将该局部变量销毁,这样就保存了外层函数的私有变量了,同时也可能会造成内存泄漏。
案例:求数组的一段区间
const arr = [1, 23, 5, 6, 34, 26, 78, 9] const a1 = arr.filter(function (item) { return item >= 2 && item <= 9 }) console.log(a1) const a2 = arr.filter(function (item) { return item >= 3 && item <= 6 }) console.log(a2)
这段代码里filter中的代码重复,可以使用闭包进行优化
function between(a, b) { return function (item) { return item >= a && item <= b } } // const between = (a, b) => (item) => item >= a && item <= b console.log(arr.filter(between(2, 9))) console.log(arr.filter(between(3, 6)))
案例:数组对象根据某个属性排序
const goods = [ { name: '苹果', price: 10, num: 52 }, { name: '梨子', price: 4, num: 200 }, { name: '芒果', price: 12, num: 150 }, { name: '香蕉', price: 8, num: 32 }, { name: '火龙果', price: 11, num: 22 }, { name: '橙子', price: 15, num: 88 } ] const priceOrder = goods.sort((a, b) => a.price - b.price) console.table(priceOrder) const numOrder = goods.sort((a, b) => a.num - b.num) console.table(numOrder)
sort函数中那段代码可以利用闭包复用
const order = (propertyName) => (a, b) => a[propertyName] - b[propertyName] console.table(goods.sort(order('price'))) console.table(goods.sort(order('num')))
内存泄漏的解决方法:
<div desc="aaa">aaa</div> <div desc="bbb">bbb</div> <script> // 要求点击div打印它的自定义属性desc const divs = document.querySelectorAll('div') // item被保存到内存中,但是并不需要它。内存中这样无用的数据多了到一定量会造成内存泄漏 // divs.forEach((item) => { // item.addEventListener('click', () => { // console.log(item.getAttribute('desc')) // console.log(item) // <div desc="aaa">aaa</div> // }) // }) // 获取到item的desc后将item设置为null,将不必要 divs.forEach((item) => { const desc = item.getAttribute('desc') item.addEventListener('click', () => { console.log(desc) console.log(item) // null }) item = null // 将item设置成null,就会被垃圾回收机制回收 }) </script>
也可以使用bind
divs.forEach((item) => { item.addEventListener('click', fn.bind(this, item)) item = null }) function fn(item) { console.log(item.getAttribute('desc')) }
this在闭包中的历史遗留问题:
const person = { username: '小明', getName: function () { console.log(this.username) // 小明 return function () { console.log(this) // window return this.username // 闭包按理来说可以访问到上级函数中的变量,但是this比较特殊。this的指向在于被谁调用,a函数是被window调用的,所以这里的this是window,所以会打印undefined } } } const a = person.getName() console.log(a()) // undefined
解决:
const person = { username: '小明', getName: function () { const _this = this return function () { console.log(_this) // {username: '小明', getName: ƒ} return _this.username // 小明 } } } const a = person.getName() console.log(a()) // 小明
或者使用箭头函数
const person = { username: '小明', getName: function () { return () => { console.log(this) // {username: '小明', getName: ƒ} return this.username // 小明 } } } const a = person.getName() console.log(a()) // 小明
【var和let/const的一个区别】:
const person = { username: '小明', getName: function() { return function() { return this.username // 小红 } } } var username = '小红' // var定义的变量会挂载到window上,let和const不会,这里如果用let或const定义,还是打印undefined const a = person.getName() console.log(a()) // 小红
案例:每隔一秒在页面打印一次当前时间
let second = 0 function counter() { return ++second } const recordSecond = setInterval(function () { if (second === 5) { clearInterval(recordSecond) console.log('计时结束') return } const str = counter() + '秒' console.log(str) }, 1000)
改成闭包
let doCounter = counter() function counter() { let second = 0 return function () { if (second === 5) { clearInterval(recordSecond) doCounter = null // 清除闭包:将引用内层函数的变量赋值为null console.log('计时结束') return } second++ console.log(second + '秒') } } const recordSecond = setInterval(function () { doCounter() }, 1000)