定义:
什么是闭包
JS忍者秘籍:闭包允许函数访问并操作函数外部的变量
红宝书:闭包是指有权访问另一个函数作用域中的变量的函数
MDN:闭包指的是那些能够访问自由变量的函数,自由变量指外部函数作用域中的变量
闭包(closure)是一个函数以及其捆绑的周边环境状态(词法环境)的引用的组合。换言之,闭包可以让开发者从内部函数访问外部函数的作用域。在JS里,闭包会随着函数的创建而被创建。闭包就是内层函数对外层函数变量的不释放。
翻译成人话:闭包就是一个函数去引用另一个函数内的变量,因为变量被引用着,所以当另一个函数执行结束时,其对应的执行上下文弹出栈的时候,变量不会被回收,因此可以用闭包来封装一个私有变量。此外,不正当地使用闭包可能会造成内存泄漏。
闭包的特征:
- 函数中包含函数
- 内部函数可以访问其外层函数的作用域
- 参数和变量不会被垃圾回收,始终留在内存中
- 有内存的地方才有闭包
闭包的作用:
- 保护变量不被垃圾回收机制销毁,保护闭包函数内的私有变量不受外部影响;
- 把函数内的值保存下来,使得方法和属性的私有化
如何形成闭包?
内部的函数存在外部作用域的引用就会导致闭包的形成
下面几行代码就是闭包最简单的实现方式,在函数outerFunc中声明了一个局部变量innerVar,而在outerFunc中声明的函数innerFunc一直在引用着局部变量innerVar和全局变量outVar,因为innerFunc函数的调用,变量innerVar和outVar无法被销毁而一直保存在内存中,这就是闭包。
let outVar = 1;
function outerFunc() {
let innerVar = 2;
function innerFunc() {
console.log(outVar, innerVar);
}
innerFunc();
}
outerFunc();
示例:
一:在函数中 return 一个函数
function funcFactory(){
var name = "Li Baitian";
function printName(){
console.log(name);
}
return printName;
};
var myFunc = funcFactory();
myFunc(); // Li Baitian
var myFunc = funcFactory() 中的 funcFactory()创建了工厂函数的执行上下文并在其中声明了一个局部变量name与一个函数praintName,最终将函数printName的引用返回,也就是将function printName(){console.log(name)}赋值给了变量myFunc
所以,当var myFunc = funcFactory()执行完成之后,相应的函数从执行上下文的栈中弹出,通常此时变量也会被销毁,但是因为myFunc的调用,导致引用着funcFactory里面的变量,所以name不会被销毁
二:立即执行函数
var add = (function() {
var counter = 0;
return function (){
return counter += 1;
}
})();
add();
add();
add(); // 3
很简单可以看出add为立即执行函数,也是一个变量,变量值为 function(){return counter += 1},add()执行完毕之后,相应的执行上下文就会从调用栈中弹出,变量counter本应该被销毁,而最终输出为3反而证明了变量在中间过程中没有被销毁,这也是闭包作用的体现。
三:定时器打印问题
一段经典代码:
for (var i = 0; i < 5; i++) {
setTimeout(() => {
console.log(i);
}, i * 1000)
}
上面的代码,我们期望的输出是:每隔一秒,依次输出0,1,2,3,4,但是得到的实际输出为5,5,5,5,5,因为ES5使用var来声明变量,带来了变量提升,全剧范围内只有一个变量i。JS单线程遇到异步的时候,代码不会先执行(会入栈),等所有同步代码执行完毕i++到5之后,异步代码才会执行,而循环结束时i=5。所以结果是5,5,5,5,5。
for (var i = 0; i < 5; i++) {
(function (j) {
setTimeout(() => {
console.log(j);
}, j * 1000)
})(i)
}
代码改成上面这样,就可以按照我们期望的方式进行工作了。这样修改之后,使用 IIFE(立即执行函数)会为每一轮循环都生成一个新的函数作用域,使得定时器函数的回调可以将新的作用域封闭在每一轮循环内部,每一轮循环内部都会含有一个具有正确值的变量可以访问。
因为闭包的存在,上面形成了五个互不干扰的私有作用域。
for(let i = 0; i < 5; i++) {
setTimeout(() => {
console.log(i)
}, i * 1000)
}
使用ES6的let也能够达到目的,因为ES6引进块级作用域,每一轮循环的变量i都是重新声明的,JS引擎会记住上一轮循环的值,初始化本轮的i时,在上一轮的基础上去进行运算
闭包的使用场景
节流
规定在一个单位时间内,只能触发一次函数,如果这个单位时间内触发多次函数,则只有一次生效
- 鼠标不断点击触发,mousedown(单位时间内只触发一次)
- 监听滚动事件,比如是否滑到底部自动加载更多,用throttle来判断
function throttle(fn, timeout) [
let timer = null;
return function (...arg) {
if(timer) return;
timer = setTimeout(() => {
fn.apply(this, arg);
timer = null;
}, timeout)
}
}
防抖
在某个事件触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时
- search搜索联想,用户在不断输入值时,用防抖来节约请求资源。
- window触发resize的时候,不断的调整浏览器窗口大小会不断的触发这个事件,用防抖来让其只触发一次
function debounce(fn, timeout) {
let timer = null;
return function(...arg){
clearTimeout(timer)
timer = setTimeout(() => {
fn.apply(this, arg);
}, timeout)
}
}
循环赋值
闭包使得下面代码能依次输出0 - 10,因为闭包会保护内部函数的私有变量,相当于这里生成了100个互相不干扰的私有作用域。
for(var i = 0; i < 10; i++) {
(function(j){
setTimeout(() => {
console.log(j);
}, 500);
})(i)
}
如果将自执行函数去掉,下面函数会输出10个10,因为JS遇到异步代码(setTimeout)会先将其入栈,等同步代码执行完成后(此时i=10),再执行setTimeout内的代码。具体哪些操作是同步/异步的,可以移步参考《宏任务与微任务》
for(var i = 0; i < 10; i++){
setTimeout(() => {
console.log(i)
}, 500)
}
实现函数柯里化
function curry(fn, len = fn.length) {
return _curry(fn, len)
}
function _curry(fn, len, ...arg) {
return function (...params) {
let _arg = [...arg, ...params]
if (_arg.length >= len) {
return fn.apply(this, _arg)
} else {
return _curry.call(this, fn, len, ..._arg)
}
}
}
let fn = curry(function (a, b, c, d, e) {
console.log(a + b + c + d + e)
})
fn(1, 2, 3, 4, 5) // 15
fn(1, 2)(3, 4, 5)
fn(1, 2)(3)(4)(5)
fn(1)(2)(3)(4)(5)
闭包造成的内存泄漏
为什么会造成内存泄漏?
JS 垃圾回收
由于字符串、对象和数组没有固定大小,所有当他们的大小已知时,才能对他们进行动态的存储分配。JavaScript程序每次创建字符串、数组或对象时,解释器都必须分配内存来存储那个实体。只要像这样动态地分配了内存,最终都要释放这些内存以便他们能够被再用,否则,JavaScript的解释器将会消耗完系统中所有可用的内存,造成系统崩溃。
function fn(){
let test = new Array(100000).fill('BaiTian')
return function(){
console.log(test)
return test
}
}
let fnChild = fn()
fn2Child()
因为return的函数中存在对fn下变量test的引用,所以test变量不会被垃圾回收,这样就造成了内存泄漏。因为闭包函数会“锁定”外部函数的变量,形成私有作用域,所以闭包函数会比其它函数占用更多的内存,过多的内存占用很危险,所以请谨慎使用闭包。
对于如何监控页面中的内存泄漏以及如何分析,可以参考这篇文章 js 内存泄漏场景、如何监控以及分析