摘要:在ES6出现之前,js中并没有块级作用域的存在,这意味者单纯一个大括号并不能隔离出一块作用域

脚本宝典收集整理的这篇文章主要介绍了

js实例教程-js闭包的使用详解

脚本宝典小编觉得挺不错的,现在分享给大家,也给大家做个参考,希望能帮助你少写一行代码,多一份安全和惬意。

小宝典致力于为广大程序猿(媛)提供高品质的代码服务,请大家多多光顾小站,小宝典在此谢过。

独立作用域

在ES6出现之前,js中并没有块级作用域的存在,这意味者单纯一个大括号并不能隔离出一块作用域

    {       var a = 1;    }

这样的大括号没有隔离出一块作用域,那么变量a声明在括号内或者括号外都是一样的,那么js中什么时候能隔离出一个局部作用域呢,答案是函数

    var b = 1;    function fn(){       var a = 1;       console.log(b);   //1    }    console.log(a)   // a is undefined

这时候函数单独隔离出了一个作用域。而函数外面的作用域在函数作用域的外层,因而函数内部能够访问到外部变量b,但函数外作用域无法访问函数内变量a。
那么在同时声明了变量a的时候会怎么样呢

    var a = 1;    function fn(){       var a = 2;       console.log(a)       }    fn();    // 2    console.log(a)   // 1

当函数内部作用域再次声明变量a的时候,这时候变量a的新声明被压入函数调用栈中,这时js引擎读取a的值时候,会读取到新的声明,所以a的值是2。而执行完函数,局部作用域的a就被弹出(变量a的生命周期结束)。上下文切换到外部作用域之后,a的值就是原来外部作用域中的a,因此输出1。
同样,把fn函数替换成一个立即执行函数(学名缩写为IIFE)效果相同

    var a = 1;    (function(){        var a = 2;        console.log(a)   // 2    })()

闭包

之前说到,函数可以访问外部作用域中的变量,但外部作用域不能访问函数内部变量。

    function fn1(){       var a = 2       function fn2(){          console.log(a);          }       return fn2;    }    var fn3 = fn1();    fn3();      // 2 这就是闭包

上面代码的fn2可以轻松访问到变量a,这个毫无疑问。当fn2的引用被赋值给fn3,那么fn3现在和fn2一样,能访问到变量a,这个也毫无疑问。然而fn3的声明却在外部作用域,这和我们上文说的外部作用域不能访问到函数内部变量相悖,这,就是闭包。
由于内部函数fn2和fn3的特殊关系,原本fn1的内部作用域原本会被销毁并被js引擎的垃圾回收器回收内存,现在fn1却能一直存活。

顽强的闭包

内部函数fn2的引用无论被传递到哪个作用域中,它都会持有对原始作用域的引用,也就是说,一直能读取到变量a

    var fn4;    function fn1(){       var a = 2       function fn2(){          console.log(a);          }       fn4 = fn2;    }    function fn3(){       fn4();     // 还是强行输出了2    }    fn3();

闭包无处不在

在定时器,事件监听器,Ajax请求或者其他异步任务中,只要使用了回调函数,实际上就是在使用闭包(回调函数被扔在事件队列中,还保存着对msg等变量的作用域引用)

    function fn(msg){       setTimeout(function(){           console.log(msg);       }, 1000);    }    fn('hello');

fn执行1000毫秒之后,它的内部作用域并不会消失,依然拥有对fn作用域的闭包。

    var btn = document.getElementById('button');    var action = 'Click';    function fn(btn, action){        btn.onclick = function(){            console.log(action);        }    }    fn(btn, action);   // 每次点击都能得到action

有一个比较常见的场景是,给循环的元素绑定事件监听函数

    var nodes = document.getElementsByTagName('p');    for(var i = 0, len = nodes.length; i < len; i++){        //这里通过一个IIFE封闭一个关于i的内部作用域        (function(i){            nodes[i].onclick = function(){                //click回调函数中通过闭包拿到i变量                alert(i);            }         })(i)    }

内存泄漏

    function Handler(){        var element = document.getElementById('someElement');        var id = element.id;        element.onclick = function(){           alert(id);        }        //只要onclick的回调匿名函数存在,element所占的内存就永远不会被回收,而我们这里只需要变量id,所以我们需要把element的引用设为null,确保正常回收占用的内存        element = null;    }

使用闭包封装变量

假设有一个计算乘积的简单函数

    var mult = function(){       var a = 1;       for(var i = 0; i < arguments.length; i++){           a = a * arguments[i];       }       return a;    }

对于那些相同的参数来说,可以使用缓存来提高效率

    var cache = {};    var mult = function(){        //mult(1, 2, 3) => '1, 2, 3'        var args = Array.prototype.join.call(arguments, ',');        if(cache[args]){            //使用cache.args会把args自动转成字符串            return cache[args];        }        var a = 1;        for(var i = 0; i < arguments.length; i++){           a = a * arguments[i];        }        return a;    }

与其让cache暴露在全局,不如将它封装在IIFE中

    var mult = (function(){        var cache = {};        return function(){            var args = Array.prototype.join.call(arguments, ',');            if(cache[args]){                //使用cache.args会把args自动转成字符串                return cache[args];            }            var a = 1;            for(var i = 0; i < arguments.length; i++){               a = a * arguments[i];            }            return a;        }    })();

提炼函数是代码重构中的一种常见技巧,如果在一个大函数中有一些代码块能够提炼出来,我们常常把这些代码块封装在独立的小函数里面,独立出来的小函数有助于代码复用,如果这些小函数有好的命名,它们本身页起到了注释的作用

    var mult = (function(){       var cache = {};       var calculate = function(){           var a = 1;       for(var i = 0; i < arguments.length; i++){            a = a * arguments[i];       }       return a;        }       return function(){          var args = Array.prototype.join.call(arguments, ',');          if(cache[args]){             return cache[args];          }          //将参数传入          return cache[args] = caculate.apply(null, arguments);       }    })()

延续局部变量的寿命

img对象经常用于数据上报

    var report = function(src){        var img = new Image();        img.src = src;    };    report('https://xxx.com/getUserInfo');

而在一些低版本浏览器中,report函数并不是每一次都成功发起了HTTP请求,原因是img是局部变量,函数结束调用后就被销毁,可能还没来得及发出HTTP请求

    var report = (function(){        var imgs = [];        return function(){           var img = new Image();           //将img放进闭包变量中           imgs.push(img);           img.src = src;            }    })()

用闭包实现命令模式

在完成闭包实现的命令模式之前,我们先用面向对象的方式来编写一段命令模式的代码

    <html>        <body>           <button id="execute">点击我执行命令</button>           <button id="undo">点击我执行命令</button>        </body>    </html>    <script>        var Tv = {            open: function(){               console.log('打开电视机');            },            close: function(){               console.log('关上电视机');            }        };        var OpenTvCommand = function(receiver){            this.receiver = receiver;        };        OpenTvCommand.prototype.execute = function(){            this.receiver.open(); //执行命令,打开电视机        }        OpenTvCommand.prototype.undo = function(){            this.receiver.close(); //撤销命令,关闭电视机        }        var setCommand = function(command){            document.getElementById('exucute').onclick = function(){               command.execute();            }            document.getElementById('undo').onclick = function(){               command.undo();             }        }        setCommand(new OpenTvCommand(Tv));    </script>

命令模式的意图是把请求封装成对象,从而分离请求的发起者和请求的接收者之间的耦合关系。在命令执行之前,可以预先往命令对象中植入命令的接收者。在闭包的模式中,命令接收者会被封闭在闭包形成的环境中

    var Tv = {       open: function(){          console.log('打开电视机');       },       close: function(){          console.log('关上电视机');       }    };    var createCommand = function(receiver){        var execute = function(){           return receiver.open(); //执行命令,打开电视机        }        var undo = function(){           return receiver.close(); //执行命令,关闭电视机        }        return {           execute: execute,           undo: undo        }    }    var setCommand = function(command){            document.getElementById('exucute').onclick = function(){               command.execute();            }            document.getElementById('undo').onclick = function(){               command.undo();             }        }        setCommand(createCommand(Tv));

执行上下文

讲完实际应用之后,下面来看一下高能的理论原理。
执行上下文是ECMAScript标准中定义的一个抽象概念,用来记录代码的运行环境。它可以是代码最开始执行的全局上下文,也可以是执行某个函数体内的上下文。
需要注意的是,程序至始至终只能进入一个执行上下文,这就是为什么js是单线程的原因,即每次只能有一个命令在执行。浏览器用栈来维护执行上下文,当前起作用的执行上下文位于栈顶,当它内部的代码执行完毕之后出栈,然后将下一个元素作为当前的上下文。
然而,程序并不需要执行完上下文中的所有代码,才能进入另一个执行上下文(在一个函数中调用另一个函数)。经常有当前的执行上下文A执行到一半暂停,又进入另一个执行上下文的情况。每次一个上下文被另一个上下文替代的时,这个新的上下文就入栈称为栈顶。
当有一堆上下文,有些执行到一半暂停的时候又继续,当继续执行的时候我们需要一种方式去记住当前的状态,事实上ECMAScript中已经做出了规定,每个执行上下文都有用来追踪执行状态的记录器

代码执行状态(Code evaluation state)在当前执行上下文中用来记录代码执行,暂停,重新执行的状态 函数(Function):当前上下文正在执行的函数体 范畴(Realm):内部对象集合,全局运行环境极其作用域下的所有代码,其他相关的状态、资源 词法环境(Lexical Environment):用来解决当前上下文中的标识符引用问题 变量环境(Variable Environment):包含环境记录(EnvironmentRecord)的词法环境,而环境变量是由变量声明(VariableStatements)所产生的

词法环境

用来定义标识符的值:词法环境的目的就是管理代码中的数据。也就是说,它给标识符赋值,让标识符变得有意义。比如,代码段console.log(x/10),如果变量x没有具体值,它是没有意义的,这段代码也没有意义。词法环境通过环境记录将标识符和具体的值联系在一起(见下一点)。 词法环境包含环境记录:环境记录完美地记录了词法环境中所有标识符和具体值之间的联系,并且每个词法环境都有自己的环境记录。 词法嵌套结构:内部环境引用包含它的外部环境,外部环境还可以有自己的外部环境。因此,一个环境可以作为多个内部环境的外部环境。全局环境是唯一一个没有外部环境的环境。

回到闭包

每个函数都有一个包含词法环境的执行上下文,它的词法环境确定了函数内的变量赋值以及对外部环境的引用。看上去函数“记住”了外部环境,但其实上是这个函数有个指向外部环境的引用。这就是“闭包”的概念。

每当外部封闭函数执行的时候就产生了闭包,也就是说闭包的创建并不一定需要内部函数返回。

JavaScript中闭包作用域是词法作用域,即它在代码写好之后就被静态决定了它的作用域。

         <link rel="stylesheet" href="https://csdnimg.cn/release/phoenix/template/css/markdown_views-ea0013b516.css">             </p

独立作用域

在ES6出现之前,js中并没有块级作用域的存在,这意味者单纯一个大括号并不能隔离出一块作用域

    {       var a = 1;    }

这样的大括号没有隔离出一块作用域,那么变量a声明在括号内或者括号外都是一样的,那么js中什么时候能隔离出一个局部作用域呢,答案是函数

    var b = 1;    function fn(){       var a = 1;       console.log(b);   //1    }    console.log(a)   // a is undefined

这时候函数单独隔离出了一个作用域。而函数外面的作用域在函数作用域的外层,因而函数内部能够访问到外部变量b,但函数外作用域无法访问函数内变量a。
那么在同时声明了变量a的时候会怎么样呢

    var a = 1;    function fn(){       var a = 2;       console.log(a)       }    fn();    // 2    console.log(a)   // 1

当函数内部作用域再次声明变量a的时候,这时候变量a的新声明被压入函数调用栈中,这时js引擎读取a的值时候,会读取到新的声明,所以a的值是2。而执行完函数,局部作用域的a就被弹出(变量a的生命周期结束)。上下文切换到外部作用域之后,a的值就是原来外部作用域中的a,因此输出1。
同样,把fn函数替换成一个立即执行函数(学名缩写为IIFE)效果相同

    var a = 1;    (function(){        var a = 2;        console.log(a)   // 2    })()

闭包

之前说到,函数可以访问外部作用域中的变量,但外部作用域不能访问函数内部变量。

    function fn1(){       var a = 2       function fn2(){          console.log(a);          }       return fn2;    }    var fn3 = fn1();    fn3();      // 2 这就是闭包

上面代码的fn2可以轻松访问到变量a,这个毫无疑问。当fn2的引用被赋值给fn3,那么fn3现在和fn2一样,能访问到变量a,这个也毫无疑问。然而fn3的声明却在外部作用域,这和我们上文说的外部作用域不能访问到函数内部变量相悖,这,就是闭包。
由于内部函数fn2和fn3的特殊关系,原本fn1的内部作用域原本会被销毁并被js引擎的垃圾回收器回收内存,现在fn1却能一直存活。

顽强的闭包

内部函数fn2的引用无论被传递到哪个作用域中,它都会持有对原始作用域的引用,也就是说,一直能读取到变量a

    var fn4;    function fn1(){       var a = 2       function fn2(){          console.log(a);          }       fn4 = fn2;    }    function fn3(){       fn4();     // 还是强行输出了2    }    fn3();

闭包无处不在

在定时器,事件监听器,Ajax请求或者其他异步任务中,只要使用了回调函数,实际上就是在使用闭包(回调函数被扔在事件队列中,还保存着对msg等变量的作用域引用)

    function fn(msg){       setTimeout(function(){           console.log(msg);       }, 1000);    }    fn('hello');

fn执行1000毫秒之后,它的内部作用域并不会消失,依然拥有对fn作用域的闭包。

    var btn = document.getElementById('button');    var action = 'Click';    function fn(btn, action){        btn.onclick = function(){            console.log(action);        }    }    fn(btn, action);   // 每次点击都能得到action

有一个比较常见的场景是,给循环的元素绑定事件监听函数

    var nodes = document.getElementsByTagName('p');    for(var i = 0, len = nodes.length; i < len; i++){        //这里通过一个IIFE封闭一个关于i的内部作用域        (function(i){            nodes[i].onclick = function(){                //click回调函数中通过闭包拿到i变量                alert(i);            }         })(i)    }

内存泄漏

    function Handler(){        var element = document.getElementById('someElement');        var id = element.id;        element.onclick = function(){           alert(id);        }        //只要onclick的回调匿名函数存在,element所占的内存就永远不会被回收,而我们这里只需要变量id,所以我们需要把element的引用设为null,确保正常回收占用的内存        element = null;    }

使用闭包封装变量

假设有一个计算乘积的简单函数

    var mult = function(){       var a = 1;       for(var i = 0; i < arguments.length; i++){           a = a * arguments[i];       }       return a;    }

对于那些相同的参数来说,可以使用缓存来提高效率

    var cache = {};    var mult = function(){        //mult(1, 2, 3) => '1, 2, 3'        var args = Array.prototype.join.call(arguments, ',');        if(cache[args]){            //使用cache.args会把args自动转成字符串            return cache[args];        }        var a = 1;        for(var i = 0; i < arguments.length; i++){           a = a * arguments[i];        }        return a;    }

与其让cache暴露在全局,不如将它封装在IIFE中

    var mult = (function(){        var cache = {};        return function(){            var args = Array.prototype.join.call(arguments, ',');            if(cache[args]){                //使用cache.args会把args自动转成字符串                return cache[args];            }            var a = 1;            for(var i = 0; i < arguments.length; i++){               a = a * arguments[i];            }            return a;        }    })();

提炼函数是代码重构中的一种常见技巧,如果在一个大函数中有一些代码块能够提炼出来,我们常常把这些代码块封装在独立的小函数里面,独立出来的小函数有助于代码复用,如果这些小函数有好的命名,它们本身页起到了注释的作用

    var mult = (function(){       var cache = {};       var calculate = function(){           var a = 1;       for(var i = 0; i < arguments.length; i++){            a = a * arguments[i];       }       return a;        }       return function(){          var args = Array.prototype.join.call(arguments, ',');          if(cache[args]){             return cache[args];          }          //将参数传入          return cache[args] = caculate.apply(null, arguments);       }    })()

延续局部变量的寿命

img对象经常用于数据上报

    var report = function(src){        var img = new Image();        img.src = src;    };    report('https://xxx.com/getUserInfo');

而在一些低版本浏览器中,report函数并不是每一次都成功发起了HTTP请求,原因是img是局部变量,函数结束调用后就被销毁,可能还没来得及发出HTTP请求

    var report = (function(){        var imgs = [];        return function(){           var img = new Image();           //将img放进闭包变量中           imgs.push(img);           img.src = src;            }    })()

用闭包实现命令模式

在完成闭包实现的命令模式之前,我们先用面向对象的方式来编写一段命令模式的代码

    <html>        <body>           <button id="execute">点击我执行命令</button>           <button id="undo">点击我执行命令</button>        </body>    </html>    <script>        var Tv = {            open: function(){               console.log('打开电视机');            },            close: function(){               console.log('关上电视机');            }        };        var OpenTvCommand = function(receiver){            this.receiver = receiver;        };        OpenTvCommand.prototype.execute = function(){            this.receiver.open(); //执行命令,打开电视机        }        OpenTvCommand.prototype.undo = function(){            this.receiver.close(); //撤销命令,关闭电视机        }        var setCommand = function(command){            document.getElementById('exucute').onclick = function(){               command.execute();            }            document.getElementById('undo').onclick = function(){               command.undo();             }        }        setCommand(new OpenTvCommand(Tv));    </script>

命令模式的意图是把请求封装成对象,从而分离请求的发起者和请求的接收者之间的耦合关系。在命令执行之前,可以预先往命令对象中植入命令的接收者。在闭包的模式中,命令接收者会被封闭在闭包形成的环境中

    var Tv = {       open: function(){          console.log('打开电视机');       },       close: function(){          console.log('关上电视机');       }    };    var createCommand = function(receiver){        var execute = function(){           return receiver.open(); //执行命令,打开电视机        }        var undo = function(){           return receiver.close(); //执行命令,关闭电视机        }        return {           execute: execute,           undo: undo        }    }    var setCommand = function(command){            document.getElementById('exucute').onclick = function(){               command.execute();            }            document.getElementById('undo').onclick = function(){               command.undo();             }        }        setCommand(createCommand(Tv));

执行上下文

讲完实际应用之后,下面来看一下高能的理论原理。
执行上下文是ECMAScript标准中定义的一个抽象概念,用来记录代码的运行环境。它可以是代码最开始执行的全局上下文,也可以是执行某个函数体内的上下文。
需要注意的是,程序至始至终只能进入一个执行上下文,这就是为什么js是单线程的原因,即每次只能有一个命令在执行。浏览器用栈来维护执行上下文,当前起作用的执行上下文位于栈顶,当它内部的代码执行完毕之后出栈,然后将下一个元素作为当前的上下文。
然而,程序并不需要执行完上下文中的所有代码,才能进入另一个执行上下文(在一个函数中调用另一个函数)。经常有当前的执行上下文A执行到一半暂停,又进入另一个执行上下文的情况。每次一个上下文被另一个上下文替代的时,这个新的上下文就入栈称为栈顶。
当有一堆上下文,有些执行到一半暂停的时候又继续,当继续执行的时候我们需要一种方式去记住当前的状态,事实上ECMAScript中已经做出了规定,每个执行上下文都有用来追踪执行状态的记录器

代码执行状态(Code evaluation state)在当前执行上下文中用来记录代码执行,暂停,重新执行的状态 函数(Function):当前上下文正在执行的函数体 范畴(Realm):内部对象集合,全局运行环境极其作用域下的所有代码,其他相关的状态、资源 词法环境(Lexical Environment):用来解决当前上下文中的标识符引用问题 变量环境(Variable Environment):包含环境记录(EnvironmentRecord)的词法环境,而环境变量是由变量声明(VariableStatements)所产生的

词法环境

用来定义标识符的值:词法环境的目的就是管理代码中的数据。也就是说,它给标识符赋值,让标识符变得有意义。比如,代码段console.log(x/10),如果变量x没有具体值,它是没有意义的,这段代码也没有意义。词法环境通过环境记录将标识符和具体的值联系在一起(见下一点)。 词法环境包含环境记录:环境记录完美地记录了词法环境中所有标识符和具体值之间的联系,并且每个词法环境都有自己的环境记录。 词法嵌套结构:内部环境引用包含它的外部环境,外部环境还可以有自己的外部环境。因此,一个环境可以作为多个内部环境的外部环境。全局环境是唯一一个没有外部环境的环境。

回到闭包

每个函数都有一个包含词法环境的执行上下文,它的词法环境确定了函数内的变量赋值以及对外部环境的引用。看上去函数“记住”了外部环境,但其实上是这个函数有个指向外部环境的引用。这就是“闭包”的概念。

每当外部封闭函数执行的时候就产生了闭包,也就是说闭包的创建并不一定需要内部函数返回。

JavaScript中闭包作用域是词法作用域,即它在代码写好之后就被静态决定了它的作用域。

         <link rel="stylesheet" href="https://csdnimg.cn/release/phoenix/template/css/markdown_views-ea0013b516.css">             </p

觉得可用,就经常来吧!Javascript技巧 脚本宝典 欢迎评论哦! js技巧,巧夺天工,精雕玉琢。小宝典献丑了!

总结

以上是脚本宝典为你收集整理的

js实例教程-js闭包的使用详解

全部内容,希望文章能够帮你解决

js实例教程-js闭包的使用详解

所遇到的程序开发问题,欢迎加入QQ群277859234一起讨论学习。如果觉得脚本宝典网站内容还不错,欢迎将脚本宝典网站推荐给程序员好友。 本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。如您有任何意见或建议可联系处理。小编QQ:384754419,请注明来意。

80%的人都看过