闭包的概念
如果一个函数访问了此函数的父级及父级以上的作用域变量,那么这个函数就是一个闭包。
所以以下写法都是闭包
var a = 1; // 匿名的立即执行函数,因访问了全局变量a,所以也是一个闭包 (function test (){ alert(a); })()
本质上,JS中的每个函数都是一个闭包,因为每个函数都可以访问全局变量。
关于JS中的作用域,可以参考博客:
https://blog.csdn.net/weixin_41192489/article/details/124277123
实现闭包最常见的方式就是函数嵌套(并不是形成闭包的唯一方式!)
function a() { var i = '初始值'; i = i + "—_执行a" // 此处的函数b访问了父级函数a中的局部变量i,成为了一个闭包 function b() { i = i + "_执行b" console.log(i) } return b; } var c = a(); // 此时 i 的值为 :初始值—_执行a c() // 此时 i 的值为 :初始值—_执行a_执行b c() // 此时 i 的值为 :初始值—_执行a_执行b_执行b
闭包的执行过程
以上方代码为例:
- 将函数a赋值给全局变量c时,a会执行一次,局部变量 i 的值变为
初始值—_执行a
,最终返回函数b,此时全局变量c的值为闭包函数b的引用。
此时函数a虽然已执行完,但因为内部包含闭包函数b,所以函数 a 的执行期上下文会继续保留在内存中,不会被销毁,所以局部变量 i 仍是初始值—_执行a
执行期上下文:当函数执行时,会创建一个执行期上下文的内部对象。每调用一次函数,就会创建一个新的上下文对象,他们之间是相互独立的。当函数执行完毕,它所产生的执行期上下文会被销毁
- 第一次执行
c()
时,闭包函数b第一次执行,局部变量 i 的值变为初始值—_执行a_执行b
- 第二次执行
c()
时,闭包函数b第二次执行,局部变量 i 的值变为初始值—_执行a_执行b_执行b
闭包的图解
var a = "global variable"; var F = function () { var b = "local variable"; var N = function () { var c = "inner local"; return b; }; return N; }; var d = F() d()
- 全局作用域 G 中有:
—— 函数 F
—— 全局变量 a
—— 全局变量 d (存有对闭包函数 N 的引用)
- 函数 F 中有: (返回闭包函数N)
—— 函数 F 作用域中的局部变量 b
—— 闭包函数 N - 闭包函数 N 中有: (返回局部变量b)
—— 函数 N 作用域中的局部变量 c
闭包的特点
1.被闭包函数访问的父级及以上的函数的局部变量(如范例中的局部变量 i )会一直存在于内存中,不会被JS的垃圾回收机制回收。
2.闭包函数实现了对其他函数内部变量的访问。(函数内部的变量对外是无法访问的,闭包通过这种变通的方法,实现了访问。)
Javascript的垃圾回收机制
- 如果一个对象不再被引用,那么这个对象就会被GC回收。
- 如果两个对象互相引用,而不再被第三者所引用,那么这两个对象都会被回收。
闭包的用途
- 访问函数内部的变量
- 让变量始终保持在内存中
闭包的应用场景
模拟面向对象的代码风格
比如模拟两人对话
function person(name) { function say(content) { console.log(name + ':' + content) } return say } a = person("张三") b = person("李四") a("在干啥?") b("没干啥。") a("出去玩吗?") b("去哪啊?")
控制台打印结果为:
张三:在干啥? 李四:没干啥。 张三:出去玩吗? 李四:去哪啊?
使setTimeout支持传参
通过闭包实现setTimeout第一个函数传参(默认不支持传参)
function func(param){ return function(){ alert(param) } } var f1 = func(1); setTimeout(f1,1000);
封装私有变量
//用闭包定义能访问私有函数和私有变量的公有函数。 var counter = (function () { var privateCounter = 0; //私有变量 function change(val) { privateCounter += val; } return { increment: function () { change(1); }, decrement: function () { change(-1); }, value: function () { return privateCounter; } }; })(); console.log(counter.value());//0 counter.increment(); console.log(counter.value());//1 counter.increment(); console.log(counter.value());//2
模拟块作用域
依次点击 4 个 li 标签,结果都弹出 4
解析:onclick绑定的function中没有变量 i,解析引擎会寻找父级作用域,最终找到了全局变量 i,for循环结束时,i 的值已变成了4,所以onclick事件执行时,全都弹出 4。
下面使用闭包来解决这个问题:
var elements = document.getElementsByTagName('li'); var length = elements.length; for (var i = 0; i < length; i++) { elements[i].onclick = function (num) { return function () { alert(num); }; }(i); }
通过匿名闭包,把每次的 i 都保存到一个变量中,实现了预期效果。
当然,通过 ES6 的 let 可以轻松解决这个问题:
var elements = document.getElementsByTagName('li'); var length = elements.length; for (let i = 0; i < length; i++) { elements[i].onclick = function () { alert(i); }; }
实现迭代器
function setup(x) { var i = 0; return function(){ return x[i++]; }; } var next = setup(['a', 'b', 'c']);
控制台中的执行效果:
> next(); "a" > next(); "b" > next(); "c"
闭包的优点
- 可以减少全局变量的定义,避免全局变量的污染
- 能够读取函数内部的变量
- 在内存中维护一个变量,可以用做缓存
闭包的缺点
1)造成内存泄露
闭包会使函数中的变量一直保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。【内存泄露:无用的变量一直在内存中,无法被释放】
解决方法——使用完变量后,手动将它赋值为null;
2)闭包可能在父函数外部,改变父函数内部变量的值。
3)造成性能损失
由于闭包涉及跨作用域的访问,所以会导致性能损失。
解决方法——通过把跨作用域变量存储在局部变量中,然后直接访问局部变量,来减轻对执行速度的影响
闭包的范例
返回匿名闭包
function funA(){ var a = 10; // funA的活动对象之中; return function(){ //匿名函数的活动对象; alert(a); } } var b = funA(); b(); //10
各自独立的闭包
function outerFn(){ var i = 0; function innerFn(){ i++; console.log(i); } return innerFn; } var inner = outerFn(); //每次外部函数执行的时候,都会开辟一块内存空间,外部函数的地址不同,都会重新创建一个新的地址 inner(); inner(); inner(); var inner2 = outerFn(); inner2(); inner2(); inner2(); //1 2 3 1 2 3
function fn(){ var a = 3; return function(){ return ++a; } } alert(fn()()); //4 alert(fn()()); //4
访问全局变量的闭包
var i = 0; function outerFn(){ function innnerFn(){ i++; console.log(i); } return innnerFn; } var inner1 = outerFn(); var inner2 = outerFn(); inner1(); inner2(); inner1(); inner2(); //1 2 3 4
写法有点绕的闭包
(function() { var m = 0; function getM() { return m; } function seta(val) { m = val; } window.g = getM; window.f = seta; })(); f(100); console.info(g()); //100 闭包找到的是同一地址中父级函数中对应变量最终的值
闭包的链式调用
var add = function (x) { var sum = 1; var tmp = function (x) { console.log('执行tmp') sum = sum + x; return tmp; } tmp.toString = function () { return sum; } return tmp; } console.log(add(1)(2)(3).toString())
控制台打印结果:
执行tmp 执行tmp 6
add(1) 时执行的是最外面的匿名函数,从(2) 开始,才执行tmp
所以第一个参数无论是几,最终结果都是6
console.log(add(8)(2)(3).toString()) // 最终结果还是 6
留意父函数执行过一次!
function love1(){ var num = 223; var me1 = function() { console.log(num); } num++; return me1; } var loveme1 = love1(); loveme1(); //输出224
打印每次链式调用的上一次传参
function fun(n,o) { console.log(o); return { fun:function(m) { return fun(m,n); } }; } var a = fun(0); //undefined a.fun(1); //0 a.fun(2); //0 a.fun(3); //0 var b = fun(0).fun(1).fun(2).fun(3); //undefined 0 1 2 var c = fun(0).fun(1); c.fun(2); c.fun(3); //undefined 0 1 1