[你不知道的javascript系列] with、eval是怎么欺骗词法作用域的?

391 阅读4分钟

编译器的第一个工作阶段叫词法话,也叫单词化。 这个阶段会对代码中的字符进行检查

这就是词法作用域的名称来源。

简单点说就是:词法作用域由你写代码的时候,将变量和块级作用域写在哪里来决定的,词法分析器处理代码的时候,会保持作用域不变

一、作用域气泡

在上面的图中,有三个作用域

淡绿色的作用域是全局作用域,它包含了一个标识符foo
橙色的作用域是foo创建的作用域,包含了三个标识符,分别是babar
蓝色的作用域是bar创建的作用域,其中只有一个标识符c

二、引擎如何查找标识符?

作用域嵌套的结构,给引擎提供了足够的查找的信息。

console.log()执行的时候,会先从bar作用域查找。如果bar作用域中,包含变量,就不会向上查找。

如果没有找到,就会继续向上查找。

作用域查找会在找到第一个匹配标识符的时候停止查找

2.1、遮蔽效应

如果在不同的作用域中,定义相同的标识符,内层的标识符会遮蔽外层的标识符(从代码运行的作用域开始)。

如果被遮蔽的标识符在全局作用域,则可以通过全局对象来获取,比如window.a。否则,被遮蔽的标识符,将无法被查询到。

2.2、作用域位置如何决定?

无论函数在什么地方被调用,也无论如何被调用,它的词法作用域都只由函数被声明时候所处的位置所决定。

三、欺骗词法

函数在运行的时候,可以修改作用域吗?

在JS中,有两种机制可以欺骗词法。分别是:eval和with

3.1、eval是如何欺骗的?

eval可以把一段字符串当做该地方的一段代码执行。

eval('console.log(1)')

输出结果:1

我们来看下面一段代码

function foo (str, a) {
    eval(str)
    console.log(a, b)
}

b = 2

foo('var b = 3;', 1)

正常来说,console.log(a, b)的时候,foo作用域中,是没有b的。会查找到全局作用域。

但是eval('var b = 3')被执行。foo在运行的时候,eval对作用域的环境进行了修改。

所以上述代码会输出:1, 3,而不是征程情况下输出的1, 2

3.2、with 是如何欺骗词法作用域的?

在聊with和作用域关系之前,我们先看一下with有什么作用。

var obj = {
    a: 1,
    b: 2,
    c: 3
}

with(obj){
    a: 2,
    b: 3,
    c: 4
}
console.log(obj)

执行结果:

我们可以看到,通过with可以做到不需要重复的引用对象本身.

with和词法作用域的关系

with 可以将一个没有或者有多个属性的对象处理称为一个完全隔离的词法作用域,这个对象的属性会被定义称为在这个作用域中的词法标识符

with语法是根据你传递的对象,创建另一个全新的词法作用域

举个例子:

function foo (obj) {
    with (obj) {
        a = 2
    }
}
var o1 = {a: 3}
var o2 = {b: 3}

foo(o1);
console.log(o1.a) // 输出3

foo(o2)
console.log(o2.a) // 输出undefined
console.log(a) // 2 内存泄露了

上述代码执行foo(o2)的时候,with创建了一个o2的全新作用域

o2的作用域中只有b这个标识符,所以,我们输出o2.aundefiend

那么a为甚会泄露到全局作用域呢?

这是因为执行a = 2的时候,a没有定义,会进行LHS引用,自动在全局创建一个a变量。

这里的LHS引用参见我的上一篇文章[你不知道的javascript系列] - 今天聊一聊什么是作用域

四、不建议使用eval和with

eval和with执行时,会创建全新的作用域,欺骗词法定义的作用域。

JS的引擎在编译阶段,对代码进行优化是根据词法解析,预先知道所有变量和函数的定义位置,才能在执行代码的时候,快速找到变量的定位位置。

因为eval和with在编译的时候,根本不能确定代码会在什么地方执行,所以无法提前确定位置,这就导致JS的引擎不会对eval和with进行优化。

所以使用eval和with带来的好处,完全不能弥补所带来的的性能上的损失。


都已经读到这里了,动动您贵手,点赞再走,祝你2021年,要啥都有。

我是阿飞,一个GTD践行者,深度工作践行者。


广告Time:

我做了一个公众号:青柠檬读书会,我希望成长的路上,有你相伴。