彻底理解闭包,闭包什么

154 阅读10分钟

一、闭包

1、闭包的基础概念

PS:以下文字不建议一开始看,或者简单看一下就行,但是肯定 理解不了的。

1)、官方解释:

一个函数和对其周围状态(变量)(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)。

闭包让你可以在一个内部函数中访问到其外部函数的作用域。

2)、通俗的来说:JavaScript中所有的function都是一个闭包。不过一般来说,嵌套的function(定义)所产生的闭包更为强大,也是大部分时候我们所谓的“闭包”。即:闭包就是嵌套函数定义。

PS1:通过以上文字描述,你是根本不会理解闭包的,特别是初学者。只有通过后面代码的阅读和测试,才能明白,请静下心来。继续阅读。 PS2:对知识的理解,必须多次阅读概念,而且是使用后多次阅读概念,不是简单的多次阅读概念。

2、闭包的语法和优缺点

1)、通过以下代码,来浅层次理解闭包。

//1、定义闭包
​
function fn1(){
    var n = 250;
    function fn2(){//fn2可以读取函数fn1的局部变量; fn2就是个闭包(这个理解有点肤浅)
        console.log(n);//250;
    }
    return fn2;//返回fn2的目的是:外部就可以调用fn2函数。
}
​
//2、调用闭包
​
let foo = fn1();//调用fn1。返回fn2,赋给foo。
foo();//相当于在调用fn2.
​
​

PS:通过以上代码,你只是直观的认识到了闭包的代码。而不能彻底理解闭包,别着急,静下心来。继续!

2)、基本调用过程及其示意图

   function fn01(){
        //1、定义变量
        let count = 100;
        //2、定义函数fn02,注意是定义函数,而不是执行函数
        function fn02(){
            count++;
            console.log("count",count);
        }
        //3、返回函数fn02
        return fn02;
    }
    
    let foo = fn01(); //这句话是在调用函数fn01,注意,调用函数fn01时,并不执行fn02的代码
    foo();  //这句话是在调用fn02,并不会重新定义count,打印101 
    foo();  //这句话是在调用fn02,并不会重新定义count  打印102
    foo();  //这句话是在调用fn02,并不会重新定义count  打印103
   

PS:以上代码的阅读,必须仔细。如果以上解释看不明白,看看下图的示意。 let foo = fn01();//只执行了蓝色框的代码,但不是不执行红色框的代码 foo() 只执行红色框的代码,并不执行红色框的代码,即:不会重新定义count变量。 所以,每次调用foo时,呈现了累加的效果

PS:以上只是知道了闭包的执行过程,你如果要进一步理解,请继续!

3)、继续写代码(重点来了)

下面代码,才能让你对闭包有一丢丢的理解,建议把上面的代码和下面的代码一定要运行以下。

    let bar = fn01(); //bar的感觉:把fn02和count绑在了一起,这就是闭包
    bar();//101
    bar();//102
       
    foo();//104
    foo();//105bar();//103
    foo();//106
   

PS: 通过以上代码的执行结果。亲,你是否有一丢丢的感觉--------好像 fn02和count绑在了一起。如果,这个感觉没有出来,请看下图。 每调用一次fn01函数,就有个fn02和count绑在一起。这就是闭包的感觉。

PS:如果以读懂以上代码了,那么,你应该对闭包有一定的感觉了。大蓝框和大红框才是真正的闭包。还记得的文章一开头对闭包的解释吗-----------一个函数和对其周围状态(变量)(lexical environment,词法环境)的引用捆绑在一起 加油,后面更精彩!!!

4)、闭包优缺点

1)、可以读取(其它)函数内部变量(局部变量) 【优点】 2)、让这些变量(外部函数的局部变量)的值始终保持在内存中【优点,也是缺点,那就需要在合适的时机使用】

扩展:通常,函数的作用域及其所有变量都会在函数执行结束后被销毁。但是,在创建了一个闭包以后,这个函数的作用域就会一直保存到闭包不存在为止。

function fn01(){
    //1、定义变量
    let count = 100;
    //2、定义函数fn02,注意是定义函数,而不是执行函数
    function fn02(){
        count++;
        console.log("count",count);
    }
    //3、返回函数fn02
    return fn02;
}
​
let foo = fn01(); //foo是全局变量(会一直在内存中),引用着局部函数fn02及其对应count。所以,count永远不会被销毁

3)、内存泄漏

由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露(有些内存不再使用了,但是,没有被释放)。

3、闭包的面试题讲解【请问你对闭包如何理解】

1)、闭包是 函数的嵌套定义,当调用外部函数时,会产生一个闭包。这个闭包就是外部函数定义的局部变量和内部函数。即:一个函数和对其周围状态(变量)的引用捆绑在一起,这样的组合就是闭包(closure)。

2)、闭包的作用:

内部函数可以使用外部函数的局部变量

让外部函数的局部变量一直存在于内存中(局部变量的生命周期变长了)。

3)、缺点:

因为,闭包会让局部变量一直保持在内存中,所以,不要滥用闭包,以防止内存泄漏。

4)、应用场景(能干啥):

  • 让数据私有化(同时使用setter和getter函数,可以保证数据的合法性)

  • 防抖

  • 节流

  • 单例模式

  • 柯里化(谨慎的说这一点) …………………… 其实用的挺多。

4、闭包进阶 - 沙箱模式( getter 和 setter 语法 )

1)、沙箱模式[用闭包实现私有属性]

ps:如果某些数据,只希望某些函数操作使用,用全局变量显然不行。

function person(){
    let age = 20;//变量age只允许 setAge函数和getAge函数使用。
    
    // 修改age
    function setAge(n){
        //限定年龄的范围在0-150之间。
        if(n<0 || n>150){
            return;
        }
        age = n;
    }
​
    // 获取age
    function getAge(){
        return age;
    }
​
    //此处只返回来函数setAge和getAge,并没有返回变量age,所以,外部不能使用变量age
    return {
        setAge,
        getAge
    }
}
​
function foo(){
    //这个函数里不能使用变量age
}
​
let p1 = person();
p1.setAge(100);
console.log(p1.getAge());//100
p1.setAge(250);
console.log(p1.getAge());//100

PS:懂其它面向对象语言的人,应该想到了,这个age变量有点像面向对象中的私有属性。对,你说的太对了。js最早就是用这种方式模拟私有属性的。 难道你没有看出来,age就是一个在函数person里的“全局”变量吗?哈哈。 面向对象中类的属性之于它的成员方法就是“全局”变量 精彩继续!!!

2)、沙箱模式的语法糖(简写)

function person() {
    let age = 1
    let name = '张三疯'const obj = {
        get age () {
            return age
        },
        set age (val) {
            age = val
        },
        get name () {
            return name
        },
        set name (val) {
            name = val
        }
    }
    return obj
}
​
​
let p = person();
​
p.name = "张四疯" //调用 set name函数。是语法糖
p.age = 20;
​
​
console.log("p.name",p.name);
console.log("p.age",p.age);

5、闭包的应用:

1)、防抖:

防抖:减少无效的代码执行。

思路:启动一个定时器(setTimeout),把要执行的代码写在定时器里。在定时器时间未到时,下次会清除上次的定时器(上次的代码不会执行)。如果时间到了,那么,会执行代码。

<!DOCTYPE html>
<html lang="en"><head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head><body><input type="text" id="search">
​
​
    <input type="text" id="search02"></body></html>
<script>let f = antiShake();
    document.querySelector("#search").onkeydown = f;
    document.querySelector("#search02").onkeydown = antiShake();
​
    // 封装的防抖
    function antiShake() {
​
        let myTimer = null;
​
        function fn() {
            if (myTimer != null) {
                clearTimeout(myTimer);
            }
​
            myTimer = setTimeout(function () {
                // 请求:问后端要数据
                console.log("请求");
            }, 200)
        }
​
        return fn;
    }
​
​
</script>

2)、节流:

节流:在多次频繁触发的情况下,只希望执行更少的次数。既就是:减少执行次数。

思路:启动定时器,前一次的定时器没有到时间(代码还没有执行),则不再启动定时器。

<!DOCTYPE html>
<html lang="en"><head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head><body style="height: 2000px;"></body></html>
<script>// 节流:在多次频繁触发的情况下,只希望执行更少的次数。既就是:减少执行次数。window.onscroll = throttle();
​
        // 封装的节流
    function throttle() {
        let myTimer = null;
​
        return function(){
            console.log("窗口的内容滚动");
            if (myTimer != null) {
                return;
            }
            myTimer = setTimeout(function () {
                console.log("执行代码");
                clearTimeout(myTimer);
                myTimer = null;
            }, 100)
        }
    }
​
</script>

6、闭包的应用进阶 - 柯理化函数

英文:currying

柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。

柯里化也可以理解为:提前接收部分参数,延迟执行,不立即输出结果,而是返回一个接受剩余参数的函数。因为这样的特性,也被称为部分计算函数。柯里化,是一个逐步接收参数的过程。

如:柯里化应该将 sum(a, b, c) 转换为 sum(a)(b)(c)。

解答一:

function sum(a){
    return function(b){
        return function(c){
            return function(d){
                return a+b+c+d;
            }
        }
    }            
}
​
let s = sum(1)(2)(3)(4);
​
console.log(s);
​

解答二:

如:柯里化应该将 sum(a, b, c) 转换为 sum(a)(b)(c)、sum(a,b)(c)、sum(a,b,c)都可以调用。

function sum(a,b,c,d){
     return a+b+c+d;
}
​
function curry(cb){
    let arrs = [];//[1,2,3,4]
    function fn(...args){   
        arrs = [...arrs,...args];//[1,2,3,4]
        if(arrs.length<cb.length){//判断实参个数是否和原函数的形参个数的关系                    
            return fn;
        }else{
            return cb(...arrs);
        }
    }
    return fn;
}
​
let sfn = curry(sum);
let s = sfn(1)(2)(3)(4);
let s = sfn(1,2)(3)(4);
let s = sfn(1,2)(3,4);
let s = sfn(1,2,3)(4)
// let s = sfn(1,2,3,4);
console.log(s);

解答三:

function sum(a,b,c,d){
     return a+b+c+d;
}
​
// 把sum(a,b,c,d)转成 sum(a)(b)(c)(d);
​
function curry(cb,arr=[]){
​
    return function(...args){
        let arrs = [...arr,...args];
        if(arrs.length<cb.length){//判断实参个数是否和原函数的形参个数的关系
            return curry.call(this,cb,arrs)//调用闭包时,传入arrs,把前面调用的状态进行保持。
        }else{
            return cb(...arrs);
        }
    }
}
​
let sfn = curry(sum);
​
// let s = sfn(a)(b)(c)(d);
// let s = sfn(1)(2)(3)(4);
// let s = sfn(1,2)(3)(4);
let s = sfn(1,2,3,4);
console.log(s);

代码欣赏:(未来再看)

//柯里化代码:
const curry = (fn, arr = []) => (...args) => (
    arg => arg.length === fn.length ? fn(...arg) : curry(fn, arg)
)([...arr, ...args])

curry()函数实际上就是完成了参数的收集和整理,最终,还是调用原函数本身。

柯里化的使用场景:

1、参数复用,减少重复参数

代码:可以使用解答二和解答三两个函数分别进行测试。

//如:在做项目时,数据可能来自好几个服务器,那么,意味着,请求地址的前缀不一样。function url(protocol, hostname, baseURL, path) {
    return protocol + hostname + "/" + baseURL + "/" + path;
}
​
function curry(fn,arr=[]){
​
    return function(...args){
        let arrs = [...arr,...args];
        if(arrs.length<fn.length){//判断实参个数是否和原函数的形参个数的关系
            return curry.call(this,fn,arrs)
        }else{
            return fn(...arrs);
        }
    }
}
​
​
let urlCurry = curry(url)
​
let douBanApi = urlCurry("https://", "www.douban.com", "doubanapi");//可以尝试是用console.log(douBanApi("login"));
console.log(douBanApi("regSave"));
console.log(douBanApi("checkUser"));
​

面试题: 1、封装一个函数,完成功能:multi(2)(3)(4)() 的结果是24, 2、封装一个函数,完成功能:add(2).multi(9).div(3) 的结果是6 console.log(myCalculator(121).add(1).minus(2).multi(3).div(4).getValue()); // 输出90