当前位置: 首页 > Web前端 > HTML

作用域(5)-块作用域

时间:2023-03-28 15:31:57 HTML

虽然函数作用域是最常见的作用域单元,当然也是当今大多数JavaScript中最常见的设计方法,但还存在其他类型的作用域单元,甚至可以实现更好的可维护性、更简洁的代码通过使用其他类型的范围界定单位。除了JavaScript之外,许多编程语言都支持块作用域,因此其他语言的开发人员会熟悉相关的思维方式,但这个概念对于主要使用JavaScript的开发人员来说会很陌生。虽然你可能连一行scoped风格的代码都不会写,但是你一定很熟悉下面这段很常见的JavaScript代码for(vari=0;i<10;i++){console.log(i);}我们定义变量i直接在for循环的头部,通常是因为我们只想在for循环的上下文中使用i,而忽略了i将在外部作用域(函数或全局)中绑定的事实。这就是块作用域的用武之地。变量声明应尽可能靠近使用它们的地方,并最大限度地本地化。另一个例子:varfoo=true;if(foo){varbar=foo*2;酒吧=东西(酒吧);console.log(bar);}bar变量仅在if语句的上下文中使用,因此如果可以在if块内声明它会很有趣。但是,当使用var声明变量时,它写在哪里并不重要,因为它们最终都在外部作用域中。这段代码是一个正式的块作用域,伪装成更具可读性的风格。如果一直是这种形式,就只能靠自觉来保证bar不被误用到作用域的其他地方了。Blockscope是用来扩展之前最小授权原则的工具,将代码从函数中隐藏信息扩展到块中隐藏信息。再次考虑for循环的示例:for(vari=0;i<10;i++){console.log(i);}为什么只在for循环内部使用的变量(至少应该使用它)只在里面)那我污染了全局函数作用域呢?更重要的是,开发人员需要检查自己的代码,避免不小心使用(或取用)范围之外的某些变量。如果变量用在了错误的地方,就会引发未知变量的异常。变量i的块作用域(如果存在)将使其仅在for循环内可用,如果在函数的其他地方使用将导致错误。这对于保证变量不会被乱用,提高代码的可维护性有很大的帮助。但遗憾的是,从表面上看,JavaScript并没有块作用域的相关功能。with我们之前讨论过with关键字。它不仅是一个难以理解的结构,而且还是一个blockscope(blockscope的一种形式)的例子【虽然不再推荐这个关键字,但还是会提到】,使用with从对象创建对象的作用域the仅在with语句中有效,在外部范围内无效。try/catch很少有人会注意到,JavaScript的ES3规范中规定的try/catch的catch子句会创建一个块作用域,其中声明的变量只在catch内部有效。例如:try{undefined();//执行非法操作强制异常【当然这种情况也可以通过thrownewError('errormessage')]}catch(err){console.log(err);//可以正常执行!}控制台日志(错误);//ReferenceError:errnotfound如你所见,err只存在于catch子句中,当试图从其他地方引用它时会抛出错误。尽管大多数标准JavaScript环境(旧版本的InternetExplorer除外)已标准化并支持此行为,但当同一范围内的两个或多个catch子句使用相同的标识符声明错误时,许多静态检查工具仍会在指定变量时发出警告。这实际上不是双重定义,因为所有变量都在安全范围内,但静态检查工具仍然会发出烦人的警告。为了避免这种不必要的警告,很多开发者会将参数命名为err1、err2等。也有开发者干脆关闭静态检查工具对重复变量名的检查。let到目前为止,我们知道JavaScript在涉及暴露模块作用域的函数时有一些奇怪的行为。如果仅此而已,JavaScript开发人员多年来就不会将块作用域作为一种非常有用的机制来使用。幸运的是,ES6改变了现状,引入了一个新的let关键字,提供了除var之外的另一种声明变量的方式。let关键字可以将变量绑定到它所在的任何范围(通常在{}内)。换句话说,由let声明的变量隐式劫持了封闭的块作用域。varfoo=true;if(foo){让bar=foo*2;酒吧=东西(酒吧);console.log(bar);}console.log(bar);//ReferenceErrorusinglettoassignavariabletoanalready在已有的blockscope上的行为是隐式的,在开发和修改代码的过程中,如果不密切关注哪些blockscope绑定了变量,习惯性的移动那些block或者将它们包装在其他块中,代码变得混乱。为块作用域显式创建块部分解决了这个问题,使变量的依赖关系更加清晰。一般来说,显式代码优于隐式代码或一些优雅但不明确的代码。【这让我想起了数据类型的转换,各有千秋,好吧,就看怎么应用了】显式块作用域风格非常好写,和其他语言的块作用域原理保持一致varfoo=true;if(foo){{//显式块letbar=foo*2;酒吧=东西(酒吧);控制台日志(栏);}}console.log(bar);//ReferenceErroraslongasstatementisvalid是的,{}括号可以在let的生命周期中的任何地方使用来创建用于绑定的块。在此示例中,我们在if语句中显式创建了一个块。如果需要重构,可以轻松移动整个块,而不会对外部if语句的位置和语义产生任何影响。【如果不在明确的{}括号内,可能存在作用域问题】小编在之前的文章中提到了promotion。提升意味着一个语句将被认为存在于它出现的范围的整个范围内。但是用let做出的声明不会在块范围内提升。在运行声明的代码之前,语句不会“存在”。{console.log(bar);//ReferenceErrorletbar=2;}在这里,我添加了一个使用var声明变量的对比,区别于let关键字{console.log(bar);//undefinedvarbar=2;}console.log(bar);//2console.log(window.bar);//2Garbagecollection块作用域非常有用的另一个原因与闭包和回收内存垃圾的回收机制有关。这里简单说明一下,内部实现原理会在以后的文章中更新。考虑以下代码:functionprocess(data){//在这里做一些有趣的事情}varsomeReallyBigData={…};process(someReallyBigData);varbtn=document.getElementById('mybutton');btn.addEventListener('click',functionclick(evt){console.log('buttonclicked');})点击函数的点击回调不需要someReallyBigData变量。理论上这意味着当进程函数执行时,在内存中占用大量空间的数据结构可以被垃圾回收。但是,由于click函数形成了一个覆盖整个范围的闭包,JavaScript引擎很可能仍然保留了这种结构(取决于具体实现)。[在我看来,这样会造成空间的浪费,同时也会导致后续其他的对这个变量的修改,造成数据不一致。严格来说这个数据是引用类型,还要考虑深浅拷贝的问题]functionprocess(data){//在这里做点有意思的事}//这个块定义的内容完成后可以销毁!{让someReallyBigData={...};process(someReallyBigData);}varbtn=document.getElementById('mybutton');btn.addEventListener('click',functionclick(evt){console.log('buttonclicked');})显式声明变量的块作用域并在本地绑定它们是添加到您的代码工具箱中的非常有用的工具。2.let循环let可以发挥优势的一个典型例子就是前面讨论的for循环。对于(让我=0;我<10;我++){console.log(i);}console.log(i);//ReferenceErrorfor循环头部的let不仅将i绑定到循环块,而且实际上它会在循环的每次迭代中重新绑定它,确保将其重新分配给它在前一次循环迭代结束时的值.这是说明重新绑定每次迭代行为的另一种方式:{letj;for(j=0;j<10;j++){让i=j;控制台日志(一);}}每次迭代都重新绑定是很有意思的,后面讨论闭包的时候再解释。由于let声明附加到一个新的作用域而不是当前函数作用域(也不是全局作用域),当代码中在函数作用域中存在对var声明的隐式依赖时,就会隐藏很多陷阱。使用let而不是var需要在代码重构期间付出额外的努力。考虑以下代码:varfoo=true,baz=10;如果(foo){varbar=3;如果(栏>栏){console.log(baz);}}这段代码可以很容易地重构为以下等价形式://所有三个变量都注册到全局窗口varfoo=true,baz=10;if(foo){varbar=3;}//保持你的眼睛打开,区别就在这里】if(baz>bar){console.log(baz);}但需要注意使用块级变量时的变化://现在只有两个全局变量,foo和巴兹。varfoo=true,baz=10;if(foo){让bar=3;if(baz>bar){//移动代码时不要忘记barconsole.log(baz);}}3.4.4constexceptlet另外,ES6还引入了const,也可以用来创建块作用域变量,但它的值是固定的(常量)。任何后续修改该值的尝试都将导致错误。[当然,引用数据类型是可以修改的。当修改值时,指向的内存地址不会改变。神奇的const可以参考小编这篇文章]varfoo=true;if(foo){vara=2;常量b=3;一=3;//正常b=4;//错误}console.log(a);//3[变量a通过var声明并注册到全局作用域]console.log(b);//ReferenceError【可见const不仅不能修改值,还继承了let的块级作用域】也可以扫描二维码,关注我的微信公众号,蜗牛全栈。