一个有意思的JS小知识点-欺骗词法

1,793 阅读6分钟

写在前面:编者为一位刚刚开始学习前端的萌新,希望通过写文记录下自己在前端学习路上的一个个脚印,也希望能通过写问和各位互相交流,如果能得到社区各位大佬的指导那就更好啦!


注:本文多为编者自学《你不知道的JavaScript》上卷,该书也被称为 小黄书(封面是黄色的),所以文中出现小黄书不要想歪......(好吧是我想歪了,最近看这名字总怪怪的)


作用域


谈到欺骗词法作用域,那首先我们肯定要想想什么是作用域。

QQ图片20210424193127.png注意:无论函数在哪里调用,也无论它如何被调用,它的词法作用域都只由函数被声明时所处的位置决定)


欺骗词法

QQ图片20210425073115.png

1.eval

先从理论上来说,JavaScript中的eval()函数会接收一个字符串作为参赛,作用就是可以让其中的内容视为好像在书写时就是存在于程序中的这个位置的代码一样,也就是“欺骗”引擎。
或者引用小黄书上的另一种说明:在执行完eval()函数之后,引擎并不“知道”或“在意”前面的代码(eval()函数接收的字符串)是以动态形式插入进来,并对词法作用域的环境进行修改的。

function foo(str,a){
    var b=2
    console.log(b);//输出2
    eval(str) //var b = 3 
              //植入,把原本不属于这个地方的代码搬到这个地方,看起来就像这里有这个一样    
              //eval欺骗词法作用域,引擎是不能自动分析  var b = 3的,但是这个函数可以,欺骗了引擎
    
    console.log(b); //输出3    
        }
        
        foo('var b = 3',1)

但是在严格模式的程序中,eval(…)在运行时有自己的词法作用域,意味着其中的声明无法修复所在的作用域

function foo(str){
	"use strict"
	eval(str)
	console.log(a)
}
foo("var a = 2")

此时打印出来的a会报错
ReferenceError:a is not defined

2.with

在看小黄书的这部分内容,我有点没看明白,之后看到一个大佬的解释很清楚:
with 语句的原本用意是为逐级的对象访问提供命名空间式的速写方式. 也就是在指定的代码区域, 直接通过节点名称调用对象。

with 通常被当做重复引用同一个对象中的多个属性的快捷方式,可以不需要重复引用对象本身。

var obj = {
        a: 1,
        b: 2,
        c: 3
        };
//如何修改obj对象中的属性?
//方法一:
        // 重复改变"obj"
         obj.a = 2;
         obj.b = 3;
         obj.c = 4;
//方法二:
        // 简单的快捷方式
        with (obj) {
        a = 3;
        b = 4;
        c = 5;
        }

在这段代码中,使用了 with 语句关联了 obj 对象,这就以为着在 with 代码块内部,每个变量首先被认为是一个局部变量,如果局部变量与 obj 对象的某个属性同名,则这个局部变量会指向 obj 对象属性。


欺骗词法的弊端

相信你们之前可能听过很多人说:在代码中使用欺骗词法不是什么好主意。为什么呢?主要是因为欺骗词法会导致数据泄漏和性能下降

导致数据泄漏

之后再看一个小黄书上的案例

function foo(obj) {
with (obj) {
a = 2;
}
}
var o1 = {
a: 3
};
var o2 = {
b: 3
};
//分开两种情况执行:
//情况一:
foo( o1 );
console.log( o1.a ); // 2
console.log( a );    //Uncaught ReferenceError: a is not defined
//情况二:
foo( o2 );
console.log( o2.a ); // undefined
console.log( a ); // 2——不好,a 被泄漏到全局作用域上了!这是执行foo(o2)的时候,创建的

with可以将一个没有或有多个属性的对象处理为一个完全隔离的词法作用域,被添加到with所处的函数作用域中
解释一下情况—、情况二发生了什么:
情况一:将o1传递给with时,with所声明的作用域是o1,这个作用域中有a属性,实际上是一个 LHS引用,将a=2赋值操作对o1.a进行赋值
情况二∶将o2传递给with时,with所声明的作用域是o2,从这个作用域开始对 a 进行 LHS查询。o2 的作用域、foo(…) 的作用域和全局作用域中都没有找到标识符 a,因此在非严格模式下,会自动在全局作用域创建一个全局变量),在严格模式下,会抛出 ReferenceError 异常。
(注:将在后续发布关于LHS和RHS查询的相关文章)

那如果变量a在全局作用域中,结果会怎样呢?

var a=1;
function foo(obj) {
        with (obj) {
        a = 2;
         }
        
}
var o1 = {
        a: 3
 };
var o2 = {
        b: 3
};
//情况一
console.log( a ); //1         全局中的a
foo( o1 );
console.log( o1.a ); // 2      o1中的a
console.log( a ); //1          全局中的a
//情况二
console.log( a ); //1           全局中的a
foo( o2 )
console.log( o2.a ); // undefined   o2中的a
console.log( a ); // 2          全局中的a被修改了

如果在函数foo()中呢?

function foo(obj) {
  var a=1;
  with (obj) {
  a = 2;
   }
  console.log( a );
}
var o1 = {
  a: 3
};
var o2 = {
  b: 3
};
// 情况一
 foo( o1 );              //1    with执行时在o1中找到了a,所以a=2对o1.a进行赋值,没有对foo()函数中的a进行赋值
console.log( o1.a );     //2   o1中的a
console.log( a ); //Uncaught ReferenceError: a is not defined //全局中没有a,外部不能访问函数foo()中的a

//情况二
foo( o2 );        //2     先在with执行时找a,没找到,向上在foo()中找,找到了,然后声明,再赋值为1,然后更改为2
console.log( o2.a );  //undefined   o2中无a
console.log( a );   // Uncaught ReferenceError: a is not defined   //这是访问全局中的a,但是全局中没有,外部不能访问函数foo()的a

注意:在严格模式下,with被严格禁止了

性能下降

这方面在小黄书的定义是因为:
JavaScript 引擎会在编译阶段进行数项的性能优化。其中有些优化依赖于能够根据代码的词法进行静态分析,并预先确定所有变量和函数的定义位置,才能在执行过程中快速找到标识符。
但如果引擎在代码中发现了 with,它只能简单地假设关于标识符位置的判断都是无效的,因为无法知道传递给 with 用来创建新词法作用域的对象的内容到底是什么。
最悲观的情况是如果出现了 with ,所有的优化都可能是无意义的。因此引擎会采取最简单的做法就是完全不做任何优化。如果代码大量使用 with 或者 eval(),那么运行起来一定会变得非常慢。无论引擎多聪明,试图将这些悲观情况的副作用限制在最小范围内,也无法避免如果没有这些优化,代码会运行得更慢的事实。

然后我找了一下能证实这一点的方法

function func() {
	console.time("func")
	var obj = {
		a: [1, 2, 3]
	};
	for(var i = 0; i < 100000; i++)
	{
		var v = obj.a[0];
	}
	console.timeEnd("func");
}

func();

function funcWith() {
	console.time("funcWith");
	var obj = {
		a: [1, 2, 3]
	};
	with(obj) {
		for(var i = 0; i < 100000; i++) {
			var v = a[0];
		}
	}
	console.timeEnd("funcWith");
}

funcWith();

结果是:

QQ图片20210424231052.png

可以看出,没有使用with的运行时间为2.43ms,使用时间为47.722ms。这也证实了会降低性能的观点。