1、前言
闭包是js中最强大的特性,也是js相较于其他语言最令人着迷的地方,如果你对它研究的透彻,你会为它着迷,否则你会被吓住。
2、闭包的前置知识点
-
垃圾回收机制:每隔一段时间,垃圾回收期会去内存中找到那些不再使用的值,然后给它释放掉,以此来缓解内存的压力。如果一个函数被全局变量引用(将函数赋值给某个全局变量),那么它将不会被垃圾回收机制回收,这种情况多了就会内存拥堵,严重时产生
内存泄漏 -
词法环境(
词法作用域): 函数的作用域基于函数声明时的位置。也就是说,当一个函数执行时和声明时的词法作用域不是同一个,闭包就产生了词法作用域小案例:
function test(fn) {
const a = 1
fn()
}
const a = 2
function fn() {
console.log(a)
}
test(fn) // 2 因为JavaScript采用的是词法作用域
tip:this的值是在函数调用时确定的,而不是函数定义时确定。而函数的作用域在函数定义时确定。
3、闭包解决了什么问题
js有回收机制,如果一个函数没有被引用,该函数执行完后他的作用域就会被销毁;如果该函数被引用了,它执行完后不会被销毁
4、闭包的定义
MDN:
一个函数和对其周围状态(Lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)。
解释: 【在JavaScript中,每当创建一个函数,闭包就会在函数创建的同时被创建出来】也就是说,所有的函数,都是闭包。整个浏览器都是一个作用域,其中的每个函数(如fn)都是作为window对象的一个方法,window对象作为父函数,fn就是子函数,调用fn时,通过前面的【window.】不写,这就是一个函数嵌套函数的结构。所有的函数都有父级,所以都是子函数,所以函数都是闭包。
JavaScript高级程序设计第三版:
闭包是值有权访问另一个函数作用域中的变量的函数。 大白话:闭包是函数。什么样的函数?可以访问到另一个函数作用域中的变量,那不就是函数嵌套函数中的子函数
JavaScript高级程序设计第四版:
闭包指的是那些引用了另一个函数作用域中变量的函数,通常是在嵌套函数中实现的。 大白话:闭包是函数。注意看,第四版相比于第三版多了一个条件——
引用了,不止是可以访问到父函数中的变量,并且引用了该变量,那么才认为这个子函数是一个闭包。MDN和第三版说的是子函数就是闭包,第四版说的是引用了父函数的变量的子函数才是闭包。
5、形成闭包的条件
- 函数嵌套函数
- 子函数引用了父函数的局部变量/局部函数(也就是说,如果b函数中没有使用a函数的变量,那么b执行的过程中不会产生闭包)
tip:旧版谷歌浏览器中即使不调用子函数也会产生闭包,新版谷歌不调用子函数时不会产生闭包
解释:
- 函数b在执行时产生了闭包,函数b就是闭包,但是这个闭包没有被保持下来,在b执行完后闭包就已经没了。
- 如果说闭包没有被保持下来,那么闭包的作用就得不到体现,同样的闭包的缺点就无从谈起了。其实我们在写代码的过程中,不知不觉就产生了闭包,那这个闭包会对内存产生压力吗?并不会,因为它压根就没有被保持下来
思考:
判断b函数在执行的过程中有没有形成闭包?
function a() {
var num = 100
var str = 'xxx'
function b() {
console.log(b) // console.log使用了a函数中的局部函数,此时会形成闭包 { b: fun }
}
b()
}
a()
/*
预编译过程:
当a调用时,产生a的AO对象
aAO: {
num: 100
str: 'xxx',
b: fun
}
当b调用时,产生b的AO对象
bAO: {} bAO是一个空对象,因为b函数中没有任何可预编译的东西
代码执行到b函数中,此时的[[Scopes]]为:
0: bAO
1: aAO
2: GO
打印的b在b函数中没有找到,向上查找,在a函数中找到局部函数b,即打印 f b() {}
*/
6、闭包的保持
- 闭包的保持可以理解为闭包状态的保持
- 如果希望在函数调用完后,闭包可以被保持下来,就需要将子函数返回到父函数的外部被全局变量接收
/*
这就是一个很经典的闭包,并且闭包的状态也会被保持:
1、函数嵌套
2、子函数使用了父函数的局部变量
3、子函数返回到父函数的外面被全局变量接收,那么此时被子函数使用的局部变量num就会被长期存储在内存中
*/
function a() {
var num = 100
return function () {
console.log(num++) // 这里形成闭包
}
}
var b = a()
console.dir(b)
b() // 100 函数b执行时产生bAO,执行完将bAO销毁,但是num的状态被保留下来了
b() // 101 再次执行b,产生新的bAO,执行完将bAO销毁,num的状态被保留
b() // 102 再次执行b,产生新的bAO,执行完将bAO销毁,num的状态被保留
7、什么时候需要使用闭包(闭包的作用)
- 一般来说,函数外是不能访问到函数内的变量的,但是通过闭包就可以访问到
- 想让一个变量长期存储在内存中,以便将来使用,但是不想定义全局变量,以免该全局变量受到污染,就要想到使用闭包
8、闭包的应用
- 在函数外访问函数的私有变量,替代使用全局变量,防止污染全局变量(闭包的保持那个例子)
- 封装私有属性和私有方法
- 回调函数的本质是利用了闭包的特性,将局部变量缓存起来了
9、内存泄漏的解决办法
<div desc="aaa">aaa</div>
<div desc="bbb">bbb</div>
<script>
// 要求点击div打印它的自定义属性desc
const divs = document.querySelectorAll('div')
// item被保存到内存中,但是并不需要它。内存中这样无用的数据多了到一定量会造成内存泄漏
// for (const item of divs) {
// item.addEventListener('click', () => {
// console.log(item.getAttribute('desc'))
// console.log(item) // <div desc="aaa">aaa</div>
// })
// }
// 获取到item的desc后将item设置为null,释放内存
for (let item of divs) {
const desc = item.getAttribute('desc')
item.addEventListener('click', () => {
console.log(desc)
console.log(item) // null
})
item = null // 将item设置成null,就会被垃圾回收机制回收
}
也可以使用bind:
const fn = item => {
console.log(item.getAttribute('desc'))
}
for (let item of divs) {
item.addEventListener('click', fn.bind(this, item))
item = null
}
10、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
解决方法一:
通过_this保存当前对象
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()) // 小明
解决方法二:
通过箭头函数,改变this指向,箭头函数的this指向它的上级this
const person = {
username: '小明',
getName: function () {
return () => {
console.log(this) // {username: '小明', getName: ƒ}
return this.username // 小明
}
}
}
const a = person.getName()
console.log(a()) // 小明
11、闭包的实际使用
1、求数组的一段区间
const arr = [1, 23, 5, 6, 34, 26, 78, 9]
const a1 = arr.filter(function (item) {
return item >= 2 && item <= 9
})
const a2 = arr.filter(function (item) {
return item >= 3 && item <= 6
})
console.log(a1)
console.log(a2)
每次获取结果都调用filter方法,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)))
2、数组对象根据某个属性排序
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')))
3、每个1秒在页面打印一次当前时间
let second = 0
const counter = () => ++second
const recordSecond = setInterval(function () {
if (second === 5) {
clearInterval(recordSecond)
console.log('计时结束')
return
}
const str = counter() + '秒'
console.log(str, new Date())
}, 1000)
改成闭包,闭包的优势——不需要定义全局变量,减少了全局变量的污染:
const counter = () => {
let second = 0
return function () {
if (second === 5) {
clearInterval(recordSecond)
doCounter = null // 清除闭包:将引用内层函数的变量赋值为null
console.log('计时结束')
return
}
second++
console.log(second + '秒', new Date())
}
}
let doCounter = counter()
const recordSecond = setInterval(function () {
doCounter()
}, 1000)
4、使用闭包实现累加函数
牢记这3点:
1. 利用了闭包的作用:将局部变量长期保存在内存中
2. 如何形成闭包:①函数嵌套函数②子函数使用了父函数的局部变量
3. 闭包如何被保持:将子函数返回到父函数外使用全局变量接收
function addFn() {
let num = 0
return function () {
console.log(num++)
}
}
const add = addFn()
add() // 0
add() // 1
add() // 2
或
function addFn() {
let num = 0
return function () {
return num++
}
}
const add = addFn()
console.log(add()) // 0
console.log(add()) // 1
console.log(add()) // 2
5、使用闭包实现私有变量
(1)通过set/get设置和读取变量
function fn() {
const obj = {}
return {
set: function (key, val) {
obj[key] = val
},
get: function (key) {
return obj[key]
}
}
}
const f = fn()
console.log(f.get('name')) // undefined
f.set('name', '小明')
console.log(f.get('name')) // 小明
构造函数的写法:
function Fn() {
const obj = {}
this.set = function (key, val) {
obj[key] = val
}
this.get = function (key) {
return obj[key]
}
}
const f = new Fn()
f.set('name', '小明')
console.log(f.get('name'))
(2)通过add/minus实现加减
function counter() {
let count = 0
return {
add: function () {
count++
},
minus: function () {
count--
},
value: function () {
return count
}
}
}
const myCounter = counter()
console.log(myCounter.value()) // 0
myCounter.add()
myCounter.add()
console.log(myCounter.value()) // 2
myCounter.minus()
console.log(myCounter.value()) // 1
第二种写法(自执行函数):
const counter = (() => {
let count = 0
return {
add: function () {
count++
},
minus: function () {
count--
},
value: function () {
return count
}
}
})()
console.log(counter.value()) // 0
counter.add()
counter.add()
console.log(counter.value()) // 2
counter.minus()
console.log(counter.value()) // 1
(3)保持用户点击链接的次数和地址---设计思路:模块化
tracker.js
let accessCounter = 0
const adAccessRec = []
// CommonJS语法导出
module.exports = {
storeAccessPage: function (page) {
adAccessRec.push(page)
},
checkAdAccessRec: function () {
return adAccessRec
},
increaseCounter: function () {
accessCounter++
},
getAccessCounter: function () {
return accessCounter
}
}
test.js
// var tracker = (function() {
// var accessCounter = 0
// var adAccessRec = []
// return {
// storeAccessPage: function(page) {
// adAccessRec.push(page)
// },
// checkAdAccessRec: function() {
// return adAccessRec
// },
// increaseCounter: function() {
// accessCounter++
// },
// getAccessCounter: function() {
// return accessCounter
// }
// }
// })()
// 将tracker的内容封装成模块
var tracker = require('./tracker') // CommonJS语法导入
console.log(tracker.getAccessCounter()) // 0
tracker.increaseCounter()
tracker.increaseCounter()
console.log(tracker.getAccessCounter()) // 2
var page1 = 'xxx'
var page2 = 'yyy'
tracker.storeAccessPage(page1)
tracker.storeAccessPage(page2)
console.log(tracker.checkAdAccessRec()) // [ 'xxx', 'yyy' ]
6、回调函数的本质是闭包
<style>
#box {
position: absolute;
}
</style>
<div id="box">hello world</div>
<script>
function animate(elementId) {
const elem = document.getElementById(elementId)
let count = 0
let timer = setInterval(function a() {
if (count < 10) {
elem.style.top = elem.style.left = count + 'px'
count++
} else {
clearInterval(timer)
timer = null
}
}, 300)
}
animate('box')
</script>
函数a就是一个闭包,它可以访问到animate函数中的elem和count
7、值缓存
const cacheBox = (function () {
const cache = {}
return {
search: function (id) {
if (id in cache) {
return '1结果' + cache[id]
}
const result = dealFn(id)
cache[id] = result
return '2结果' + result
}
}
})()
function dealFn(id) {
console.log('这是一段比较耗时的操作')
return id
}
// 第一次执行cacheBox.search(100),cache中没有100,走dealFn函数,比较耗时;第二次执行cacheBox.search(100),cache中已经存在100,直接输出
let res = cacheBox.search(100)
console.log(res) // 2结果100
let res1 = cacheBox.search(100)
console.log(res1) // 1结果100
8、for循环中的定时器
for (var i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i)
}, 100)
}
以上这段代码将打印3次3,并非想象中的 0 1 2
原因:定时器中的函数使用了本函数外的变量i,导致变量i被缓存到内存中不被销毁,这里不是因为异步导致的
如果需要打印对应的下标i:
- 定时器中可以放函数的调用,此时函数不受定时器的控制,直接执行打印
- 使用闭包
- 使用ES6的
let,具有块级作用域
9、打印li对应的下标
DOM:
<ul id="ul">
<li>第一个</li>
<li>第二个</li>
<li>第三个</li>
</ul>
JS:
const ul = document.getElementById('ul')
const lis = ul.getElementsByTagName('li')
for (var i = 0; i < lis.length; i++) {
lis[i].onclick = function () {
console.log(i)
}
}
此时,每次点击li打印的都是3。循环的执行是一瞬间的,而事件的回调是点击后才触发,此时i已经是3了
如果需要打印对应的下标i:
- 使用闭包
或
- 使用ES6的let
- 循环时保存i
- 使用bind
- 使用forEach代替for
10、定时器函数传参
// 定时器中function无法直接实现传参
setTimeout(params => {
console.log(params) // undefined
}, 100)
// 通过闭包实现定时器传参
function fn(params) {
return function () {
console.log(params) // 100
}
}
var one = fn(100)
setTimeout(one, 100)
扩展:利用定时器第三个参数传参
// 定时器的第三个参数可以传参
function one(params) {
console.log(params) // 200
}
setTimeout(one, 1000, 200)
11、vue中computed传参
需求:页面中展示本周的日期,并且让今天的日期变为红色
通过闭包的方式传值:
- computed中的方法需要return一个函数,该函数可以接收到参数
- 使用computed中的方法,一般是将函数放这(无括号),这里要写函数的调用(有括号)
<template>
<div>
<ul>
<li v-for="(date,index) of list" :key="index" :style="{color:today(date)?'red':''}">{{date}}</li>
</ul>
</div>
</template>
<script>
export default {
methods: {
// 获取本周日期
getWeekDataList() {
const weekList = []
const date = new Date()
const currentDate = date.getDate() // 今天是多少号
const weekDate = date.getDay() // 今天是周几
date.setDate(
weekDate == '0' ? currentDate - 6 : currentDate - weekDate + 1
)
let myDate = date.getDate() // 本周周一是多少号
let myMonth = date.getMonth() + 1
if (myDate < 10) myDate = '0' + myDate
if (myMonth < 10) myMonth = '0' + myMonth
weekList.push(date.getFullYear() + '-' + myMonth + '-' + myDate)
// 获取周二以后日期
for (var i = 0; i < 6; i++) {
date.setDate(myDate + 1)
myDate = date.getDate()
myMonth = date.getMonth() + 1
if (myDate < 10) myDate = '0' + myDate
if (myMonth < 10) myMonth = '0' + myMonth
weekList.push(date.getFullYear() + '-' + myMonth + '-' + myDate)
}
return weekList
}
},
data() {
return { list: this.getWeekDataList() }
},
computed: {
today() {
return function (date) {
return date.slice(-2) == new Date().toString().slice(8, 10)
}
},
// 这种方式接收不到参数
today1(date) {
return date.slice(-2) == new Date().toString().slice(8, 10)
}
}
}
</script>
12、闭包的总结
-
什么是闭包:函数嵌套函数,并且子函数引用了父函数的局部变量,其中子函数就是闭包(JavaScript高级程序设计第4版)。要注意,在新版谷歌浏览器中,子函数必须要调用才能产生闭包。
-
闭包的特点:
①闭包可以访问到父函数的局部变量,这其实是作用域链的特性
②闭包很常见,通常一个函数被创建出来,闭包就会同时被创建出来(MDN这么说的)。回调函数都是闭包,一般的闭包没有实现长期保存局部变量的特性,所以也不会对内存产生大的压力
-
闭包的作用:让局部变量长期存储在内存中,以便将来使用,避免了过多的使用全局变量
-
如何才能让一个局部变量长期保存在内存中:将子函数返回到父函数的外面用一个全局变量接收
-
如何缓解内存泄漏的风险:当获取到需要的数据后将变量设置为null
-
this在闭包中的指向:记住this永远都是谁调用该方法,方法内的this就指向谁。闭包中的this可能与你想象中的不同,为了尽可能避免这种问题,推荐使用箭头函数
-
闭包的应用:实现私有变量全局化,避免全局变量的污染;回调函数的本质是闭包;computed传参