脚本宝典收集整理的这篇文章主要介绍了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中已经做出了规定,每个执行上下文都有用来追踪执行状态的记录器
每个函数都有一个包含词法环境的执行上下文,它的词法环境确定了函数内的变量赋值以及对外部环境的引用。看上去函数“记住”了外部环境,但其实上是这个函数有个指向外部环境的引用。这就是“闭包”的概念。
每当外部封闭函数执行的时候就产生了闭包,也就是说闭包的创建并不一定需要内部函数返回。
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中已经做出了规定,每个执行上下文都有用来追踪执行状态的记录器
每个函数都有一个包含词法环境的执行上下文,它的词法环境确定了函数内的变量赋值以及对外部环境的引用。看上去函数“记住”了外部环境,但其实上是这个函数有个指向外部环境的引用。这就是“闭包”的概念。
每当外部封闭函数执行的时候就产生了闭包,也就是说闭包的创建并不一定需要内部函数返回。
JavaScript中闭包作用域是词法作用域,即它在代码写好之后就被静态决定了它的作用域。
<link rel="stylesheet" href="https://csdnimg.cn/release/phoenix/template/css/markdown_views-ea0013b516.css"> </p
觉得可用,就经常来吧!Javascript技巧 脚本宝典 欢迎评论哦! js技巧,巧夺天工,精雕玉琢。小宝典献丑了!
以上是脚本宝典为你收集整理的js实例教程-js闭包的使用详解全部内容,希望文章能够帮你解决js实例教程-js闭包的使用详解所遇到的问题。
本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
如您有任何意见或建议可联系处理。小编QQ:384754419,请注明来意。