当前位置: 首页 > 科技观察

你对Javascript的作用域和闭包了解多少?

时间:2023-03-16 14:01:47 科技观察

本文转载自微信公众号《前端引力》,作者一川。转载本文请联系前端Gravity公众号。1写在前面在Javascript编程中,闭包是一个非常重要又难以理解的概念,它与作用域知识密切相关。那么:范围在Javascript中意味着什么?在什么场景下使用闭包?如何通过定时器循环输出自增数,如何通过JS代码实现?2ScopeScope:指变量作用域,即变量可以被访问的作用域。在es5之前,只有:全局作用域和函数作用域。es6后新增:block-levelscope2.1全局作用域在Javascript中,全局变量是挂载在window顶层对象下的变量,因此可以在网页的任何地方使用和访问这个全局变量。functiongetName(){varname="inner";console.log(name);//"inner"}getName();console.log(name);//nameisnotdefined使用全局变量的缺点是:当定义很多全局变量时,很容易造成变量命名冲突。因此,在定义全局变量时,要注意作用域的访问范围。2.2函数作用域函数作用域:除了函数内部可以访问,其他地方不能访问。而且这个函数执行完之后,定义在函数作用域内的局部变量也会被销毁。functiongetName(){varname="inner";console.log(name);//"inner"}getName();console.log(name);//nameisnotdefined2.3Block-levelscope最直接的block-levelscope表现是:新增了let关键字,使用let关键字定义的变量只能在块级作用域内访问。这是因为它具有临时死区的特点:这个变量在定义之前是不能使用的。在Javascript中,if语句、try...catch...、switch和for语句后面的{...}都是块级作用域。console.log(name);//nameisnotdefinedif(true){letname="yichuan";console.log(name);//"yichuan"}console.log(name);//nameisnotdefined3闭包在《Javscript高级程序设计》这里闭包是如何定义的:闭包是一个函数,它可以访问另一个函数范围内的变量。它在MDN中是这样定义的:一个函数与其周围状态(词法环境,lexicalenvironment)的引用捆绑在一起(或者函数被引用包围),这样的组合就是一个闭包。也就是说,闭包允许您从内部函数内部访问外部函数的范围。在JavaScript中,无论何时创建一个函数,都会在创建该函数的同时创建一个闭包。对于我们的理解,闭包实际上是一个可以访问其他函数内部变量的函数。通常函数的内部变量是不能在函数外访问的,所以使用闭包的作用就是突破这个限制,使其具有可以在函数外访问函数内部变量的功能。functionfunc1(){varname="yichuan";returnfunction(){console.log(name);}}func1();//这里返回一个函数functionvarresult=func1();//用result接收这个返回的结果functionresult();//1我们看到上面的代码其实具有闭包的特点,即name的值可以在func函数外访问到。下面分析闭包的原因,这需要先了解什么是作用域链。作用域链:当访问一个变量时,代码解释器会先在当前作用域中搜索,如果没有找到,就会去父作用域中查找,直到找到该变量或者在父作用域中不存在该变量。varname="outer";console.log(name);//outerfunctionfunc1(){varname="inner";console.log(name);//innerfunctionfunc2(){varname="self";console.log(name);//self}functionfunc3(){console.log(name);//inner}func2();func3();}functionfunc4(){functionfunc5(){console.log(name);//outer}}func1();函数4();我们看到如果在函数func2中使用了name变量,他就会在func2中找到name变量,并使用它的值。函数func3中没有name变量,它会沿着作用域链查找func1函数中的name变量并使用。在函数func5中,如果查找的时候没有name变量,就会在func4函数中查找name变量,然后到全局变量中去寻找name变量并使用。闭包生成的本质是在当前环境中存在对父作用域的引用。functionfunc1(){varname="inner";functionfunc2(){console.log(name);//inner}returnfunc2;}varresult=func1();result();是不是只有返回函数的形式才会产生闭包?包裹呢?当然不是,只要对父作用域的引用存在。varfunc2;functionfunc1(){varname="yichuan";func2=function(){console.log(name);}}func1();func2();//"yichuan",那么闭包有哪些形式呢?直接返回一个函数只要在定时器、事件监听器、ajax请求、webworker或任何异步进程中使用回调函数,实际上,函数IIFE是以将闭包作为函数参数传递的形式立即执行的,closure被创建,保存了全局作用域窗口和当前函数作用域,所以可以输出全局变量//TimersetTimeout(function(){console.log("yichuan");},1000);//事件监听constcontainer=document.getElementById("app");container.addEventListener("click",function(){console.log("eventlistener");});//函数参数传递varname="yichuan";functionfunc1(){varname=“一川”;functionfunc2(){console.log(name);}func3(func2);}functionfunc3(func){func();//closure}func1();//输出“onechuan”//立即执行函数varname="yihchuan";(function(){console.log(name);//"yichuan"})();那么,我们应该如何应用闭包来解决循环问题呢?预计打印出来的是1、2、3、4、5吗?for(vari=1;i<=5;i++){setTimeout(function(){console.log(i);},0);}答案是否定的,最后打印出来的是五个6。为什么?这是因为:setTimeout是一个宏任务。由于js中的单线程EventLoop机制,宏任务要等到主线程同步任务执行完毕后才会执行,所以循环结束后会依次执行setTimeout中的回调。setTimeout函数也是一个闭包,它的父作用域是沿着作用域链的window,变量i是window上的全局变量。在开始执行setTimeout函数之前,变量i已经变成了6,所以最终的输出值为56s。那么如何解决呢?//使用立即执行函数for(vari=1;i<=5;i++){(function(j){setTimeout(function(){console.log(j);},0);})(i)}//在es6中使用letfor(leti=1;i<=5;i++){setTimeout(function(){console.log(i);},0);}想到的方法就是利用了setTimeout隐藏的第三个参数。for(vari=1;i<=5;i++){setTimeout(function(){console.log(i);},0,i);}附加参数,一旦定时器超时,它们将作为参数传递给功能。但要注意:IE9及更早版本的IE浏览器不支持向回调函数传递附加参数(第一种语法)。5参考文章《Javascript核心原理精讲》《MDN》《Javascript高级程序设计》6写在后面闭包的使用经常出现在日常的javascript编程中,使用场景多且复杂,需要读者仔细分析。这篇文章讲讲作用域和闭包的知识。根据变量的作用域,作用域分为:全局作用域、函数作用域和块级作用域。闭包是一个可以访问其他函数内部变量的函数。解决for循环打印的问题,可以使用立即执行函数,setTimeout的第三个参数,es6中的let关键字来解决。注意:闭包在实际开发中要注意不要滥用,容易造成内存泄漏。