开篇
作为前端开发人员,闭包一定是大家面试绕不开的知识点,在我们实际开发的过程中我们也会有意无意接触到闭包。本文来带着大家一步步从闭包的概念梳理到其应用场景。
概念
MDN官方中文文档对于闭包的概念是这样定义的------
闭包 (closure)是一个函数以及其捆绑的周边环境状态(lexical environment ,词法环境)的引用的组合。换而言之,闭包让开发者可以从内部函数访问外部函数的作用域。在 JavaScript 中,闭包会随着函数的创建而被同时创建。
下面我们来复习一下闭包相关的一些基础概念
作用域
分为全局作用域以及局部作用域(包括块级作用域以及函数作用域),在他们中定义的变量分别称为全局变量以及局部变量。
案例如下:
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
var name = "Marry" // name 是全局变量
function init() {
var name = "Mozilla"; // name 是一个被 init 创建的局部变量
console.log(name)
}
init()
</script>
</body>
</html>
显然,此时的输出结果是Mozilla,因为两者都存在时自己的局部变量的优先级是比全局变量高的
自由变量
自由变量可以理解为不在自己作用域中的变量,示例如下:
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div>nihao</div>
</body>
<script>
var name = "Marry"
function init() {
console.log(name); //此时的name是自由变量
}
init()
</script>
</html>
那么此时name的值是函数的定义时外层作用域的变量值,name的值与调用的地方是没有关系的。这一点在闭包中是十分重要的概念。
触发场景
知道了闭包的概念以及相关的基础知识以后,让我们来看看哪些情况下会产生闭包。
函数当作返回值被返回
直接上代码,创建返回的函数时,name是其的自由变量,于是我们在他的外层作用域去找对应的name变量,找到了"Ccat"。
虽然我在后面全局作用域中也定义了一个变量name,此时返回的函数已经形成了他的闭包,不会受到外界name变量的影响了,最终输出"Ccat"
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<script>
function getName(){
const name = "Ccat"
return function(){
console.log(name);
}
}
const name = "Mouse"
const fun = getName()
fun()
</script>
</html>
函数当作返回值被返回
这是第二种触发场景,按照之前的分析方法分析一遍就可以得到正确的输出结果了。
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
</body>
<script>
function fn(cb) {
const name = "mouse"
cb()
}
const name = "Ccat"
fn(
function () {
console.log(name);
}
)
</script>
</html>
自定义匿名函数
第三种是自定义匿名函数,后面的案例会有应用。
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
</body>
<script>
(function(index){
console.log(index);
})(100)
</script>.
</html>
应用场景
解决var for循环
大家在学习js基础部分的时候可能会遇到这么一个问题,我通过for循环来为我的元素添加点击事件,点击后输出其对应的index,但是事情并非所愿,如果按照下方的代码去写的话无论我点击哪一个按钮输出的都是3。
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<button>0</button>
<button>1</button>
<button>2</button>
</body>
<script>
const buttonItems = document.getElementsByTagName("button")
for (var index = 0; index < buttonItems.length; index++) {
buttonItems[index].onclick = function(){
console.log(index)
}
}
</script>
</html>
这种情况的发生是因为我们的点击事件回调函数没有形成自己的闭包,导致当用户点击时,index的值始终为for循环执行后的值,也就是3。
定位到了问题所在,我们的方向就是让回调函数形成自己的闭包,这里使用的是自定义匿名函数的方式,代码如下:
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<button>0</button>
<button>1</button>
<button>2</button>
</body>
<script>
const buttonItems = document.getElementsByTagName("button")
for (var index = 0; index < buttonItems.length; index++) {
(function (index) {
buttonItems[index].onclick = function () {
console.log(index)
}
})(index)
}
</script>
</html>
问题解决!当然我们也可以采用现在开发中最常用的方法,把var改成let,形成块级作用域,这里就不再过多赘述了。
隐藏变量
所谓闭包,就是能够将信息封闭起来。第二个应用场景我们来封装一个具有set、get功能的函数。代码如下:
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<script>
function newData() {
const data = {}
return {
get: function (key) {
return data.key
},
set: function (key, val) {
data.key = val
}
}
}
const dataSet = newData()
dataSet.set("name","哈哈哈")
const name = dataSet.get("name")
console.log(name);
</script>
</html>
当我们在外部想访问函数内部的data时是访问不到的,因为函数形成了闭包。
节流与防抖
这个在业务中就很常见了
节流就是n秒内只运行一次,若在n秒内重复触发,只有一次生效,防抖则是n秒后在执行该事件,若在 n 秒内被重复触发,则重新计时。
节流实现如下(时间戳+定时器结合):
js
function throttled(fn, delay) {
let timer = null
let starttime = Date.now()
return function () {
let curTime = Date.now() // 当前时间
let remaining = delay - (curTime - starttime) // 从上一次到现在,还剩下多少多余时间
let context = this
let args = arguments
clearTimeout(timer)
if (remaining <= 0) {
fn.apply(context, args)
starttime = Date.now()
} else {
timer = setTimeout(fn, remaining);
}
}
}
防抖实现如下:
js
function debounce(func, wait, immediate) {
let timeout;
return function () {
let context = this;
let args = arguments;
if (timeout) clearTimeout(timeout)
if (immediate) {
let callNow = !timeout; // 第一次会立即执行,以后只有事件执行后才会再次触发
timeout = setTimeout(function () {
timeout = null;
}, wait)
if (callNow) {
func.apply(context, args)
}
}
else {
timeout = setTimeout(function () {
func.apply(context, args)
}, wait);
}
}
}