1.引子
JS有着自身独特的变量作用域,其特殊之处就在于:
- 函数内部可以直接读取全局变量
var n=999;
function f1(){
alert(n);
}
f1(); // 999
- 函数外部无法读取函数内局部变量
此处函数内如果不使用var命令声明变量相当于声明全局变量
function f1(){
var n=999;
}
alert(n); // error
那该如何从外部也能读取函数内局部变量呢,那就是在函数的内部再定义一个函数。
function f1(){
var n=999;
function f2(){
alert(n); // 999
}
}
f2是可以访问f1中的变量n的,那么只要return f2,就可以在f1外部读取到f1内的局部变量了。
function f1(){
var n = 999;
return function f2(){
console.log(n);
}
}
var result = f1();
result(); //999
//不能写下面这行代替上面两行!
f1()();
而这个f2函数,就是‘闭包’。
1.1 匿名函数
上面的代码也可以把f2省略掉,那就涉及到匿名函数的概念,匿名函数1是可以用于闭包,二是可以形成块级作用域
没有名字的函数就是匿名函数,听着陌生其实实际应用中很常见了,比如
setInterval(()=>{
},100)
第一个参数就是匿名函数
而匿名函数写成立即执行函数就可以封装出块级作用域
function fn(){
(function(){
var la="啦啦啦!";
})();
console.log(la);//报错---la is not defined
}
fn();
2.闭包的概念
闭包是能够读取其他函数内部变量的函数
因为只有函数内部的子函数才能读取它的变量,所以可以把闭包理解为:
定义在一个函数内部的函数
3.闭包范例
闭包可以用在许多地方。它的最大用处有两个,一个是前面提到的可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中。
3.1 解绝循环打印问题
改进方法一:立即执行函数
使用 IIFE(立即执行函数)会为每一轮循环都生成一个新的函数作用域,使得定时器函数的回调可以将新的作用域封闭在每一轮循环内部,每一轮循环内部都会含有一个具有正确值的变量可以访问
改进方法二:let替换var
变量i是let声明的,当前的i在本轮循环有效,所以每一次循环的i其实都是一个新的变量。JavaScript 引擎内部会记住上一轮循环的值,初始化本轮的变量i时,就在上一轮循环的基础上进行计算。
3.2 一道经典闭包题目
const fn = function(n , o){
console.log('o' , o);
return {
test : function(m){
return fn(m , n)
}
}
}
const a = fn(0);//undefined
a.test(1);//0
a.test(2);//0
a.test(3);//0
const b = fn(0).test(1).test(2).test(3);//undefiend 0 1 2
首先分析a这个变量:
const a = fn(0);//undefined
fn函数有两个参数function(n , o),现在只传入第一个参数n=0,那么o就是undefined,因此打印出o为undefined。
a.test(1);//0
test是fn中的闭包,它传入一个参数m,返回fn(m , n)。
这里执行a.test(1),也就是使得m=1,而n的值在上方定义时已经定义为0,因此相当于test将return fn(1,0)。
因此,a.test(1),就相当于执行fn(1,0),打印出来的o就是0。
同理a.test(2)就是执行fn(2,0),打印出来的o还是0,以此类推。
随后分析b这个变量:
const b = fn(0).test(1).test(2).test(3);//undefiend 0 1 2
首先fn(0).test(1)和上方变量a的a.test(1)道理相同,因此首先输出undefined和0。
随后是fn(0).test(1).test(2),此时test传入2,使得m=2。
而上一步fn(0).test(1)相当于return(1,0),此时n=1,o=0,因此fn(0).test(1).test(2)相当于return(2,1)。也就相当于const fn = function(2,1),因此将1作为fn中o的参数,打印出1。
接下来则以此类推打印出2。
3.3 闭包中的this
fn中的this在非严格模式中指向window,严格模式中undefined
var obj = {
birth: 1990,
getAge: function () {
var b = this.birth; // 1990
var fn = function () {
return new Date().getFullYear() - this.birth; // this指向window或undefined
};
return fn();
}
};
将fn改成箭头函数,继承了上层中的this
var obj = {
birth: 1990,
getAge: function () {
var b = this.birth; // 1990
var fn = () => new Date().getFullYear() - this.birth; // this指向obj对象
return fn();
}
};
obj.getAge(); // 25
4.内存泄露
闭包可能导致严重的内存泄露问题
一般情况下,函数执行会形成一个新的私有的作用域,当私有作用域中的代码执行完成后,我们当前作用域都会主动的进行释放和销毁。但当遇到函数执行返回了一个引用数据类型的值,并且在函数的外面被一个其他的东西给接收了,这种情况下一般形成的私有作用域都不会销毁。
像这种情况:
function fn() {
var num = 100;
return function () {}
}
var f = fn();//fn执行形成的这个私有的作用域就不能再销毁了
fn内部的私有作用域会被一直占用,导致内存泄露,需要手动将引用变量指向null。
像这道内存泄露的经典题目:
function outer() {
var num = 0;//内部变量
return function add() {
num++;//内部函数有引用,作为add函数的一部分了
console.log(num);
};
}
var func1 = outer();
func1();//实际上是调用add函数, 输出1
func1();//输出2 因为outer函数内部的私有作用域会一直被占用
var func2 = outer();
func2();// 输出1 每次重新引用函数的时候,闭包是全新的。
func2();// 输出2
5.闭包的应用场景
任何闭包的使用场景都离不开这两点:
- 创建私有变量
- 延长变量的生命周期
一般函数的词法环境在函数返回后就被销毁,但是闭包会保存对创建时所在词法环境的引用。
5.1 函数节流和防抖
详细解释见函数节流和防抖博客,这里就把防抖的代码贴上来
function debounce(func , delay){
let timeout = null;
return function(e){
console.log(e)
//在规定时间间隔到达前,一触发就重新计时
clearTimeout(timeout);
//每次触发事件就会新增一个setTimeOut开始计时,经过规定时间后执行func
timeout = setTimeout(() =>{
func.apply(this , arguments);
} , delay);
}
}
function test(){
console.log('debounce' , Math.random());
}
window.addEventListener('scroll' , debounce(test , 500))
5.2 封装私有变量
如下面这个例子中的sum外部是无法访问到的。
function f1(){
let sum = 0;
return {
fn : function(){
sum++;
console.log(sum);
}
}
}
//f1返回一个对象传递给res
let res = f1();
//res通过'.'操作符运行对象中的方法属性
res.fn(); //1
res.fn(); //2
res.fn(); //3
5.3 函数柯里化
柯里化的目的在于避免频繁地调用具有相同参数的函数,同时又能够轻松地重用。
const getArea = function(width , height){
console.log(width * height);
}
//假如我们要处理的长方形的宽总是10
const rec1 = getArea(10 , 20);
const rec2 = getArea(10 , 30);
//可以使用闭包柯里化来计算面积
const getArea1 = width =>{
return height => {
console.log(width * height);
}
}
const getTenWidthArea = getArea1(10);
//之后只要是求宽度为10的长方形面积就传长这个参数就好了
const rec3 = getTenWidthArea(40);
5.4 计数器
对象中能持续访问到外层的cur,注意还能访问到init,因为闭包既能访问外层函数的局部变量,也能访问外层函数的参数。
var createCounter = function(init) {
let cur = init;
return {
increment: () => ++cur,
decrement: () => --cur,
reset: () => (cur = init),
}
};
6. 修改闭包中的内容?
看到这么一道很有意思的题目,要求在不修改下面这段代码的情况下修改obj的内容:
let o = (function () {
let obj = {
a: 1,
b: 2,
};
return {
get: function (k) {
return obj[k];
},
};
})();
漏洞在于闭包没有对obj[k]能访问到的内容作出限制,所以可以让obj[k]访问到obj原型上的方法,因此我们可以给原型添加一个访问器方法,用来获取到闭包中被保护的obj:
Object.defineProperty(Object.prototype, "evil", {
get() {
return this;
},
});
let o2 = o.get("evil");
o2.a = 1111111111;
console.log(o.get("a"));
要解决这个问题对属性进行一下判断即可,必须是要自身携带的属性才返回:
let o = (function () {
let obj = {
a: 1,
b: 2,
};
return {
get: function (k) {
if (obj.hasOwnProperty(k)) {
return obj[k];
}
return undefined;
},
};
})();