关于JavaScript的闭包
1.概述
1.1 概述
闭包实际上是函数内部和函数外部接触的一道桥梁。它的形式是在一个函数中返回另外一个内部函数,让内部函数可以访问外部函数的属性。
1.2 闭包的定义
闭包是指在创建时捕获了周围词法作用域中的变量的函数。这些变量可以在闭包内部被访问和修改,即使闭包在其词法作用域外被调用。
1.3 词法作用域
词法作用域是指作用域在代码编写时已经确定,而不是在函数调用时确定。函数在定义时就“记住”了它所在的作用域。
1.4 基本结构
function outerFunction() {
let outerVariable = 'I am from outer scope';
function innerFunction() {
console.log(outerVariable);
}
return innerFunction;
}
let closure = outerFunction();
closure(); // 输出: I am from outer scope
在上面的示例中,innerFunction形成了一个闭包,它捕获了outerFunction的词法作用域中的变量outerVariable。
2.闭包的工作原理
2.1 词法作用域
词法作用域是指作用域在代码编写时已经确定,而不是在函数调用时确定。函数在定义时就“记住”了它所在的作用域。这方面学习知识见我写的文章中的第六节。
2.2 执行上下文和作用域链
每次调用一个函数时,JavaScript 引擎会创建一个新的执行上下文,并为其创建作用域链。当内部函数试图访问外部变量时,它会沿着作用域链查找该变量。这种机制使得闭包成为可能,即函数能够访问到定义时的作用域中的变量,即使它被外部调用。
2.3 闭包的持久化
在闭包中,即使外部函数已经执行完毕,内部函数仍然保留了对外部变量的引用。这是因为 JavaScript的垃圾回收机制不会回收闭包中引用的变量,除非没有其他引用。
3.闭包的应用场景
3.1 模拟计数器
function counter(){
let count=0;
return function (){
return ++count;
}
}
const inc=counter();
console.log(inc())//1
console.log(inc())//2
console.log(inc())//3
3.2 封装私有变量和方法
闭包可以用于数据封装和信息隐藏,保护变量不被外部代码直接访问和修改。
function createPerson(name) {
let age = 0;
return {
getName: function() {
return name;
},
getAge: function() {
return age;
},
growOlder: function() {
age++;
}
};
}
const person = createPerson('John');
console.log(person.getName()); // 输出: John
console.log(person.getAge()); // 输出: 0
person.growOlder();
console.log(person.getAge()); // 输出: 1
3.3 封模块化开发
举一个简单的例子:有三个文件1.js,2.js和test.html。
//下面是1.js
var a=1;
//下面是2.js
var a=2;
下面是test.html:
<script src="./2.js"></script>
<script src="./1.js"></script>
<script>
console.log(a)//2,script顺序不一样会导致输出的值不一样
</script>
这里面两个文件中都有a,当同时在某个文件引入时候,则必定会产生覆盖现象。在真正的开发中,每个 文件的内容很多,这时候如果要考虑每个文件中变量是否冲突会很麻烦。
下面进行优化:
//下面是1.js
var module1=(function(){
var a=1;
return function(){
return a;
}
})();
//下面是2.js
var module2=(function(){
var a=2;
return function(){
return a;
}
})();
//下面是test.html
<script src="./2.js"></script>
<script src="./1.js"></script>
<script>
var aFn=module1;
var bFn=module2;
console.log(aFn())//1
console.log(bFn())//2
</script>
下面是test.html
<script src="./2.js"></script>
<script src="./1.js"></script>
<script>
var aFn=module1;
var bFn=module2;
console.log(aFn())//1
console.log(bFn())//2
</script>
将每个文件中所有东西放到立即执行函数中去用一个变量存储。这样子只用让每个文件中的这个变量不一样就可以。
3.4 回调函数
闭包在处理异步操作和回调函数时非常有用,尤其是当需要在异步操作完成后访问一些上下文信息时。
function fetchData(url) {
setTimeout(function () {
console.log(`Fetching data from ${url}`);
}, 2000);
}
fetchData('https://api.example.com');
在这个例子中,setTimeout 内部的匿名函数形成了一个闭包,它能够访问外部函数的 url 变量,即使 fetchData 已经执行完毕。
4.其他注意点
4.1 闭包的形成条件
- 函数嵌套
- 外部函数返回内部函数: 外部函数必须返回内部函数,这样内部函数才能作为闭包存在。
- 内部函数使用了外部函数的变量: 内部函数不仅需要访问外部函数的变量,还需要在外部函数返回后继续使用这些变量
4.2 一个对比
function counter(){
let count=0;
return function (){
return ++count;
}
}
const inc=counter();
console.log(inc())//1
console.log(inc())//2
console.log(inc())//3
function counter(){
let count=0;
return function (){
return ++count;
}
}
console.log(counter()())//1
console.log(counter()())//1
console.log(counter()())//1因为闭包没有用变量缓存
4.3 立即执行函数表达式(IIFE)(也叫自执行函数)
-
概念
定义函数之后立即调用该函数 -
两种写法
(function(){ console.log(2) })();//注意分号不能漏掉 (function(){ console.log(2) }())
-
应用
现实中都是协同开发,会相互产生干扰,因此我们在开发中希望尽量减少直接在全局作用域中编写代码;
4.4 闭包与循环的经典例子
4.4.1 示例1
function foo(){
var arr=[];
for (var i=0;i<10;i++){
arr[i]=function (){
return i
}
}
return arr
}
var bar=foo();
console.log(bar[0]());//10
console.log(bar[1]());//10
console.log(bar[2]());//10
这个例子中很多人都会理解错,很多人会以为输出0,1,2。这个分析要从作用域和执行环境栈开始分析。在执行环境栈中有三个作用域(全局作用域
,foo作用域
,和匿名作用域
);全局作用域中存着foo
,bar
;foo
作用域中存着arr
、i
;而匿名函数作用域中的i
是自由变量,i
在foo
作用域中;一开始执行foo
时候完毕后,arr
已经完全赋值好,i
最终等于10
;随后执行bar[0]
,这里面的i
在当前作用域不存在,需要在上一级作用域foo
中拿到,而此时上一级作用域中i
为10
。
就相当于下面这样子:
function foo1(){
var i=0
let fn1=function (){
return i
}
i=1;
let fn2=function (){
return i
}
i=2;
let fn3=function (){
return i
}
console.log(fn1());//2
console.log(fn2());//2
console.log(fn3());//2
}
4.4.2 示例2
function foo2(){
var arr=[];
for (var i=0;i<10;i++){
arr[i]=function(x){
return function(){
return x;
}
}(i)
}
return arr
}
var bar2=foo2();
console.log(bar2[0]());//0
console.log(bar2[1]());//1
console.log(bar2[2]());//2
4.4.3 示例3
function foo3(){
var arr=[];
for (var i=0;i<10;i++){
(function(x){
arr[x]=function (){
return x;
}
})(i)
}
return arr
}
var bar3=foo3();
console.log(bar3[0]());//0
console.log(bar3[1]());//1
console.log(bar3[2]());//2
4.4.4 示例4
//使用let
function foo(){
var arr=[];
for (let i=0;i<10;i++){
arr[i]=function (){
return i
}
}
return arr
}
var bar=foo();
console.log(bar[0]());//0
console.log(bar[1]());//1
console.log(bar[2]());//2
4.5 闭包的其它示例
返回值:
function fn1(){
var name="wlm";
return function (){
return name
}
};
var bar1=fn1();
console.log(bar1())
函数赋值(一种变形):
var bar2;
function fn2(){
var name="wlm";
var func2=function (){
return name
}
bar2=func2;
};
fn2()
console.log(bar2())
函数传参(一种变形):
function bar3(callback){
console.log(callback())
};
function fn3(){
var name="wlm";
var func3=function (){
return name
}
bar3(func3);
};
fn3()
IIFE(以函数传参为例):
function bar4(callback){
console.log(callback())
};
(function(){
var name="wlm";
var func4=function (){
return name
}
bar4(func4);
})()
循环赋值:
function fn5(){
var arr=[];
for (var i=0;i<10;i++){
arr[i]=(function (x){
return function(){
return x
}
})(i)
}
return arr;
}
var bar5=fn5();
console.log(bar5[2]())//2
getter和setter:
let getValue,setValue;
(function(){
var num=0;
getValue=function (){
return num;
}
setValue=function (val){
if (typeof val =="number"){
num=val
}else{
throw new Error("类型错误");
}
}
})()
console.log(getValue());//0
setValue(10);
console.log(getValue());//10
setValue(aaa);//Uncaught ReferenceError: aaa is not defined
迭代器:
先看看计数器,两者思路一样。
function counter(){
var num=0;
return function (){
return ++num;
}
}
var add=counter()
console.log(add())//1
console.log(add())//2
再看看迭代器:
function setUp(arr){
var num=0;
return function (){
return arr[num++]
}
}
var next=setUp(["wlm","zjl","ldh"]);
console.log(next())
console.log(next())
console.log(next())
区分首次:
var firstLoad=(function (){
var list=[];
return function (val){
if (list.indexOf(val)>=0){
return false
}else{
list.push(val)
return true;
}
}
})()
console.log(firstLoad(10))//true
console.log(firstLoad(10))//false
console.log(firstLoad(11))//true
console.log(firstLoad(12))//true
缓存机制:
let sum=(function (){
var cache={};
var calulate=function (){
//类数组也适合这个循环
let count=0;
for (var i=0;i<arguments.length;i++){
count+=arguments[i]
}
return count;
}
return function (){
var args=Array.prototype.join.call(arguments,",");
if (args in cache){
return cache[args];
}
return cache[args]=calulate.apply(null,arguments);//arguments是类数组
}
})();
console.log(sum(1,2,3,1,1,2,3))//13
console.log(sum(1,2,3,1,1,2,3))//13
console.log(sum(1,2,3,1,1,2,3,2))//15
函数柯里化:
a.例子1:
function uri_curring(protocol){
return function (hostname,pathname){
return `${protocol}${hostname}${pathname}`
}
}
const uri_https=uri_curring("https://");
const uri1=uri_https("www.baidu.com","/点赞")
const uri2=uri_https("www.baidu.com","/投币")
const uri3=uri_https("www.baidu.com","/收藏")
console.log(uri1,uri2,uri3)
例子2(浏览器兼容性检测):
const whichEvent=(function (){
if (window.addEventListener){
return function (element,type,callback,useCapture){
element.addEventListener(type,function (e){
callback.call(element,e);
},useCapture);
}
}else{
return function (element,type,callback){
element.attachEvent('on'+type,function (e){
callback.call(element,e);
});
}
}
})();
例子3:
function add(){
let args=Array.prototype.slice.call(arguments);
function inner(){
args.push(...arguments)
return inner;
}
inner.toString=function (){
return args.reduce(function (pre,cur){
return pre+cur;
})
}
return inner;
}
//必须要有一个加号
console.log(+add(1))//1
console.log(+add(1,2))//3
console.log(+add(1,2)(3))//6
console.log(+add(1)(2)(3)(4)(5))//15
console.log(add(1)(2)(3)(4)(5)); //返回一个函数
例子4(可以看出来函数式编程思想):
const namelist1=[
{mid:"wlm1",profession:'中单'},
{mid:"wlm2",profession:'中单'},
{mid:"wlm3",profession:'中单'},
{mid:"wlm4",profession:'中单'},
]
const namelist2=[
{adc:"轮子妈",profession:'ADC'},
{adc:"VN",profession:'ADC'},
{adc:"老鼠",profession:'ADC'},
]
// console.log(namelist1.map(hero=>hero.mid))
// console.log(namelist2.map(hero=>hero.adc))
//这个返回的是一套规则
const curring =function (name){
return function (element){
return element[name]
}
}
const name_mid=curring("mid");
const name_adc=curring("adc");
//获取数组中所以对象的mid属性
console.log(namelist1.map(name_mid))//['wlm1', 'wlm2', 'wlm3', 'wlm4']
//获取数组中所以对象的adc属性
console.log(namelist2.map(name_adc))//['轮子妈', 'VN', '老鼠']
5.闭包的优缺点
5.1 优点
- 数据封装:闭包可以将变量封装在函数作用域中,保护它们不被外部代码访问。
- 模块化:闭包可以用于创建模块,将相关的功能和数据封装在一起。
- 保持状态:闭包可以在多个函数调用之间保持状态,非常适合用于计数器等场景。
5.2 缺点
- 内存消耗:闭包会保留对其词法作用域中变量的引用,可能导致内存泄漏。
- 调试困难:由于闭包的存在,调试时可能很难追踪变量的来源和变化。
6.闭包在实际开发中的最佳实践
- 谨慎使用全局变量
在使用闭包时,尽量减少对全局变量的依赖,这样可以避免作用域链变得复杂,也能提升代码的可维护性。 - 控制闭包的生命周期
在不再需要某个闭包时,及时释放它所持有的变量引用,避免内存泄漏。通过手动设置变量为 null 或使用 WeakMap 等工具,可以帮助控制闭包的生命周期。 - 避免过度使用闭包
虽然闭包在许多场景下非常有用,但不应滥用闭包。过度依赖闭包可能导致代码变得难以理解和维护。因此,应根据实际需求谨慎选择是否使用闭包。
7.闭包与类的区别
范围:
- 闭包:常见于函数式编程中。闭包通常用于封装状态、实现私有变量等功能。
- 类:类是面向对象编程中的重要概念,用于描述具有相似属性和行为的对象的模板。
封装方式:
- 闭包:通过函数和其引用环境来封装状态和行为;
- 类:通过属性和方法来封装数据和操作。
状态的保存方式:
- 闭包通过引用环境来保存状态
- 而类通过实例变量和类变量来保存状态