要理解闭包
,首先必须理解Javascript特殊的变量作用域
一、作用域
作用域类型:
全局作用域
、函数作用域
、ES6中新增了块级作用域
,作用域最大的用处就是隔离变量
,不同作用域下同名变量不会有冲突函数作用域
是指声明在函数内部的变量,函数的作用域在函数定义的时候就决定了块作用域
- 块作用域由
{ }
包括,if和for语句里面的{ }
也属于块作用域- 在块级作用域中,可通过let和const声明变量,该变量在指定块的作用域外无法被访问
1.1 var、let、const的区别
var定义的变量,没有块的概念,可以跨块访问, 可以变量提升
let定义的变量,只能在块作用域里访问,不能跨块访问,也不能跨函数访问,无变量提升,不可以重复声明
const用来定义常量,使用时必须初始化(即必须赋值),只能在块作用域里访问,而且不能修改,无变量提升,不可以重复声明
// i是var声明的,在全局范围内都有效,全局只有一个变量i,输出的是最后一轮的i值,也就是 10
var a = [];
for (var i = 0; i < 10; i++) {
a[i] = function() {
console.log(i);
};
}
a[0](); // 10
// 用let声明i,for循环体内部是一个单独的块级作用域,相互独立,不会相互覆盖
var a = [];
for (let i = 0; i < 10; i++) {
a[i] = function() {
console.log(i);
};
}
a[0](); // 0
1.2 作用域链
当查找变量的时候,首先会先从当前上下文的变量对象(作用域)中查找,如果没有找到,就会从父级的执行上下文的变量对象中查找,如果还没有找到,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做
作用域链
let x0 = 0;
(function autorun1(){
let x1 = 1;
(function autorun2(){
let x2 = 2;
(function autorun3(){
let x3 = 3;
console.log(x0 + " " + x1 + " " + x2 + " " + x3);//0 1 2 3
})();
})();
})();
二、闭包
闭包就是能够读取其他函数内部变量的函数
由于在Javascript语言中,只有函数内部的
子函数
才能读取局部变量,因此可以把闭包简单理解成"定义在一个函数内部的函数"
。 所以,在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。
function f1(){
var n=999;
function f2(){
alert(n);
}
return f2;
}
var result=f1();
result(); // 999
函数f2就被包括在函数f1内部,这时f1内部的所有局部变量,对f2都是可见的,既然f2可以读取f1中的局部变量,那么只要把f2作为返回值,我们不就可以在f1外部读取它的内部变量了,f2函数,就是闭包。
闭包的用途:
闭包可以用在许多地方。它的最大用处有两个,一个是前面提到的可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中。
function f1(){
var n=999;
nAdd=function(){n+=1}
function f2(){
alert(n);
}
return f2;
}
var result=f1();
result(); // 999
nAdd();
result(); // 1000
在这段代码中,result实际上就是闭包f2函数。它一共运行了两次,第一次的值是999,第二次的值是1000。这证明了,函数f1中的局部变量n一直保存在内存中,并没有在f1调用后被自动清除。
为什么会这样呢?原因就在于f1是f2的父函数,而f2被赋给了一个全局变量,这导致f2始终在内存中,而f2的存在依赖于f1,因此f1也始终在内存中,不会在调用结束后,被垃圾回收机制(garbage collection)回收。
这段代码中另一个值得注意的地方,就是"nAdd=function(){n+=1}"这一行,首先在nAdd前面没有使用var关键字,因此nAdd是一个全局变量,而不是局部变量。其次,nAdd的值是一个匿名函数(anonymous function),而这个匿名函数本身也是一个闭包,所以nAdd相当于是一个setter,可以在函数外部对函数内部的局部变量进行操作。
三、闭包的应用
// 原始题目
for (var i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i); // 1s后打印出5个5
}, 1000);
}
// ⬅️利用闭包,将上述题目改成1s后,打印0,1,2,3,4
// 方法一:
for (var i = 0; i < 5; i++) {
(function(j) {
setTimeout(function timer() {
console.log(j);
}, 1000);
})(i);
}
// 方法二:
// 利用setTimeout的第三个参数,第三个参数将作为setTimeout第一个参数的参数
for (var i = 0; i < 5; i++) {
setTimeout(function fn(i) {
console.log(i);
}, 1000, i); // 第三个参数i,将作为fn的参数
}
// ⬅️将上述题目改成每间隔1s后,依次打印0,1,2,3,4
for (var i = 0; i < 5; i++) {
setTimeout(function fn(i) {
console.log(i);
}, 1000 * i, i);
}
1. 防抖
在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。
实现思路:
每次都执行闭包函数,只要有延时器id就清除掉 首次执不执行通过flag去控制
应用场景: 搜索框输入文字后调用对应搜索接口
function debounce(fn, delay, flag = false) {
let timer;
return function (...args) {
// 只要有timer就清除掉
timer && clearTimeout(timer)
// 是否首次执行函数调用
if (flag && !timer) fn.apply(this, args)
// 第一次指定没有timer 创建延时时间范围后执行函数调用
// this指向window 注意用的箭头函数 this由外部函数作用域决定
timer = setTimeout(() => {
fn.apply(this, args)
}, delay)
}
}
// 代码测试
function test(n) {
console.log(`我执行了${n}次`);
}
let result = debounce(test, 2000, true)
for (let i = 0; i < 5; i++) {
result(i)
}
2. 节流
规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效。
实现思路:
每次都执行闭包函数,判断时间范围内有没有延时器id 没有去开启延时调用函数 同时将id=null 首次执不执行通过flag去控制 进入判断将开关 关闭
应用场景: 下拉滚动加载
function throttle(fn, time, flag = false) {
let timer;
return function (...args) {
// 首次是否执行
if (flag) {
fn.apply(this, args)
flag = false
}
// 没有timer创建延时执行 每次执行完将timer = null 这样保证时间范围内只创建一个
if (!timer) {
timer = setTimeout(() => {
fn.apply(this, args)
timer = null
}, time)
}
}
}
// 代码测试
function test(n) {
console.log('我执行了');
}
let result = throttle(test, 3000, true)
setInterval(() => result(2), 500);
四、闭包的垃圾回收
副作用:不合理的使用闭包,会造成内存泄露(就是该内存空间使用完毕之后未被回收)
闭包中引用的变量直到闭包被销毁时才会被垃圾回收
好文推荐: