关于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作用域中存着arri;而匿名函数作用域中的i是自由变量,ifoo作用域中;一开始执行foo时候完毕后,arr已经完全赋值好,i最终等于10;随后执行bar[0],这里面的i在当前作用域不存在,需要在上一级作用域foo中拿到,而此时上一级作用域中i10

就相当于下面这样子:

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.闭包与类的区别

范围:

  • 闭包‌:常见于函数式编程中‌。闭包通常用于封装状态、实现私有变量等功能。
  • 类‌:类是面向对象编程中的重要概念,用于描述具有相似属性和行为的对象的模板。

封装方式:

  • 闭包:通过函数和其引用环境来封装状态和行为;
  • 类:通过属性和方法来封装数据和操作。

状态的保存方式:

  • 闭包通过引用环境来保存状态
  • 而类通过实例变量和类变量来保存状态
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值