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

作用域(三)——欺骗词法

时间:2023-03-28 19:05:55 HTML

上一篇小编介绍了词法作用域,并提到了两个会“欺骗”词法作用域的关键字——eval,今天小编就来揭开这两个关键字的神秘面纱你。在探索今天的内容之前,让我们先还清上一篇的债。在上一篇文章中,我提到了[通过这种技术,可以访问被同名变量遮蔽的全局变量,但如果遮蔽了非全局变量,则无论如何都无法访问。】,下面的代码就不写出来了。就是这样:varb=3functionfoo(){varb=4;函数bar(){varb=5;控制台日志(b);}bar();}在这个函数中,可以调用functionfoo,可以看到词法作用域的阴影效果。在这个函数中,全局变量varb=3被遮蔽了,foo中的varb=4也被遮蔽了;我们可以通过window.b访问到3,但是目前为止,我们还没有访问到4。今天的干货正式开始:如果词法作用域在写代码的时候完全由函数声明的位置来定义,我们如何在运行时“修改”(或欺骗)词法角色?领域?JavaScript中有两种机制可以实现这一点。社区的普遍共识是,在您的代码中使用这两种机制并不是一个好主意。但是关于它们的争论通常忽略了最重要的一点:在词法作用域上作弊会导致性能不佳。在详细解释性能问题之前,我们先了解一下这两种机制的原理。1.evalJavaScript中的eval函数可以接受一个字符串作为参数,并将内容当作是在程序的这个位置写的一样。换句话说,您可以在您编写的代码中以编程方式生成代码并运行它,就好像代码是在该位置编写的一样。【其实这本身似乎就影响了代码的正常运行。】基于这个原理来理解eval,它是如何通过代码欺骗来修改词法作用域环境,假装代码在写的时候就在那里(也就是词法周期),这个原理就变得清晰易懂了。在执行eval之后的代码时,引擎并不“知道”也不“关心”前面的代码是以动态形式插入的[即通过eval函数内部的代码],修改了词法作用域的环境.引擎将像往常一样进行词法范围查找[欺骗引擎]。考虑以下代码functionfoo(a){eval(str);//作弊[因为我们不知道str中会传递什么,它可能是一个新的作用域]console.log(a,b);}varb=2;foo("varb=3",1);//代码“varb=3;”在1,3eval的调用中会被当做是存在的[此时,函数引用时就变成这样了]functionfoo(a){varb=3;console.log(a,b);}varb=2;foo(1);//1,3由于该代码声明了一个新变量b,因此它修改了现有foo的词法范围。事实上,这段代码实际上在foo中创建了一个变量b,以与之前相同的方式在外部(全局)范围内隐藏同名变量。[你还记得变量的阴影效应吗?不记得可以看小编之前的文章】执行console.log时,会同时在foo内部找到a和b,但永远找不到外部的b。[因为在eval内部,建立了一个localscope来覆盖全局b。如果要访问全局b,可以通过window.b访问]这样就会输出"1,3"而不是正常输出【如果要强制输出"1,2",我们可以调皮的改一下代码]functionfoo(a){eval(str);console.log(a,window.b);//通过窗口,访问全局变量b的值}varb=2;foo("varb=3",1);//上面例子中的1,2,为了展示的方便和简洁,我们传入的“code”字符串是固定的。在实际情况下,很容易根据程序逻辑将字符串动态拼接在一起,然后传入。eval通常用于执行动态创建的代码,因为动态执行像例子中固定字符串组成的代码并不多比直接在那里编写代码更有用。在严格模式下的程序中,eval在运行时有自己的词法作用域,这意味着其中的声明不能修改作用域foo(str){"usestrict";评估(海峡);控制台日志(一);//ReferenceError:a未定义}foo("vara=2");JavaScript中还有其他函数与eval非常相似。setTimeout和setInterval的第一个参数可以是字符串,字符串的内容可以理解为动态生成的函数代码。这些功能已弃用,不推荐使用。不要使用它们!newFunction函数的行为类似,最后一个参数可以接受一个代码字符串并将其转换为一个动态生成的函数(前一个参数是这个新生成函数的形式参数)。这个构造函数的语法比eval稍微安全一些,但是很少在程序中尽量避免使用动态生成的代码,因为它带来的好处无法抵消性能上的损失。2.withJavaScript中另一个难以掌握(现在不推荐)的特性是with关键字。有很多方法可以解释。这里我选择从这个角度来解释它:它是如何与它所影响的词法作用域相互作用的。with通常用作重复引用同一对象中的多个属性而不重复引用对象本身的快捷方式。例如:varobj={a:1,b:2,c:3};//乏味地重复“obj”obj.a=2;obj.b=3;obj.c=4;//简单快捷方式with(obj){a=3;b=4;c=5;}但它真的不仅仅是为了方便访问对象属性。考虑以下代码:functionfoo(obj){with(obj){a=2;}}varo1={a:3};varo2={b:3};foo(o1);console.log(o1.a);//2foo(o2);控制台日志(o2.a);//undefinedconsole.log(a)//2a泄漏到全局范围。在此示例中,创建了两个对象o1和o2。其中一个有财产,另一个没有。foo函数接受一个obj参数,它是一个对象引用,并在此对象引用上执行with(obj){}。在with块中,我们编写的代码看起来像是对变量a的简单词法引用,它实际上是一个LHS引用并将2赋值给它。当我们传入o1时,a=2赋值操作找到o1.a并赋值2给它,可以在后面的console.log(o1.a)中体现出来。而传入o2时,o2没有属性,所以不会创建这个属性,o2.a保持undefined状态。但是您会注意到一个奇怪的副作用。实际上,a=2赋值操作创建了一个全局变量a。这里发生了什么?with可以把一个没有或者有多个属性的对象当作一个完全隔离的词法作用域,所以这个对象的属性也会被当作定义在这个作用域中的词法标识符来处理。虽然with块可以把一个对象当作一个词法作用域,但是这个块内部的普通var声明并不局限于这个块的作用域,而是被添加到with所在函数的作用域中。如果eval函数接受包含一个或多个声明的代码,它将修改它所在的词法作用域,而with语句实际上根据你传递给它的对象凭空创建了一个全新的词法作用域。可以这样理解,当我们把o1传给with时,with声明的作用域就是o1,这个作用域包含了一个与o1.a属性相匹配的标识符。但是当我们使用o2作为范围时,它里面没有标识符,所以执行正常的LHS标识符查找。在o2的作用域、foo的作用域、全局作用域都没有找到标识符a,所以当执行a=2时,自动创建一个全局变量(因为是非严格模式)【严格模式下,它会报ReferenceError]和这种将对象及其属性放入一个作用域中并同时分配标识符的行为非常令人费解。但为了说明我们所看到的,这是我能给出的最直接的解释。不推荐使用eval和with的另一个原因是它们会受到严格模式的影响(限制)。with是完全禁止的,并且在保留核心功能的同时也禁止对eval的简洁或不安全使用。3.性能eval和with会在运行时修改或创建新的作用域,以欺骗其他在编写时定义的词法作用域。那又怎样,你可能会问?如果他们可以实现更复杂的功能并且代码更具可扩展性,那不是很好吗?答案是否定的。JavaScript引擎在编译阶段执行多项性能优化。其中一些优化依赖于能够从词法上静态分析代码并预先确定所有变量和函数的定义位置,以便在执行期间可以快速找到标识符。但是如果引擎在代码中发现了eval或者with,那么只能简单的假设对标识符位置的判断是无效的,因为在词法分析阶段无法准确知道eval会接收到哪些代码,以及这些代码如何代码将起作用。范围,没有办法知道传递给创建新词法范围的对象的内容是什么。最悲观的情况是如果出现eval或者with,所有的优化可能都没有意义,所以最简单的办法就是根本不做任何优化。【因为中间不确定因素太多,最好的办法就是原地踏步,保持原状】如果代码中大量使用eval或者with,肯定会跑得很慢。无论引擎多么聪明地试图将这些悲观情况的副作用保持在最低限度,它都无法避免如果进行这些优化,代码将运行得更慢的事实。也可以扫描下方二维码关注我微信公众号,蜗牛全栈