你知道什么是词法作用域以及如何“欺骗”它吗?

887 阅读6分钟

图片.png

今天我给大家介绍一个在这门语言中比较重要一个概念:什么是词法作用域及何为欺骗词法作用域?,文章是参考《你不知道的JavaScript(上卷)》做的一些学习笔记,发表一些个人的理解与大家进行分享交流,存在不足的地方还望各位大佬能够批评指正。

前言

在介绍词法作用域前我们抛出一个概念“词法作用域是作用域的一种工作模型

JavaScript中的词法作用域是比较主流的一种,另一种动态作用域(比较少的语言在用bash脚本等), 动态作用域我们这就不做讲解主要介绍一下词法作用域这个概念。

我们要知道没有作用域也就没用词法作用域

  • 那什么是作用域呢?

作用域是一套规则,用来管理引擎如何在当前作用域以及嵌套的子作用域中根据标识符名称进行变量查找

关于作用域的介绍我到时候会写一篇文字进行详细介绍。

词法作用域

词法作用域 就是在词法分析时定义的作用域,在写代码时,由变量和块作用域的位置决定。它是静态的,词法分析处理代码时会保持作用域不改变(欺骗词法作用域除外)。

  • 词法阶段 这是一个三级嵌套的作用域

QQ图片20210425173409.png

①,②,③分别为全局作用域,函数foo作用域,函数bar作用域,作用域的范围 是根据作用域代码块定义的位置决定的。 当我们去执行console.log(a,b,c,d)操作时,由于词法作用域在词法分析阶段就确定了,那么就先会在当前作用域下查找这4个变量,如果找不到就会在外层嵌套的作用域中继续查找(作用域链),直至找到该变量,或抵达最外层作用域为止。

如果我们在foo函数中bar函数添加一段 var c = 5,请问最终c会输出几呢?

var d
function foo(a){

    var b =  a*2
    var c = 5;
    function bar(c){
        cosole.log(a,b,c,d);
    }
    bar(b*3)
}

答案还是12

为什么不是取外面那个C的值呢?这就要引入"遮蔽效应"的概念了:

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

因为在执行console当前作用域内能找到C这变量了。如果这C为全局变量的话可以通过window.C访问到,否则C被遮蔽后无论如何都无法被访问到。

词法作用域查找只会查找一级标识符, 比如 a 、 b 、 c 。如果代码中引用了 foo.bar,词法作用域只会查找 foo 标识符,找到后,对象属性访问规则 再对 bar的属性进行访问。

欺骗词法作用域

如果词法作用域完全由写代码期间函数所声明的位置来决定,怎样才能在运行时来"修改"(欺骗)词法作用域呢?

在JavaScript中有两种这样的的机制来实现这个目的,分别是evalwith。但是不推荐使用因为它们会导致性能下降

eval

  • eval(..)函数可以接受一个字符串为参数,并将其中的内容视为好像在书写时就存在于程序中这个位置的代码。
  • 在执行eval(..)之后的代码时,引擎并不"知道"和"在意"前面的代码时动态的插入进来,并对词法作用域的环境进行了修改,只会一如既往的进行词法作用域查找。

我们来看以下代码:

function foo(str,a){
       eval(str)//相当于植入了 var b = 3
       console.log(a,b)
  }
var b = 2
foo('var b=3',1)// 1,3

eval(..)调用的"var b = 3",这段代码会被插入到foo函数中去,这段代码会被当做本来就在那里一样处理。由于这段代码中声明了一个新的b变量,因此对foo函数的词法进行了修改。这就是前面我们提到的"遮蔽效应",foo函数里定义了一个新的变量b,并遮蔽了全局作用域中的同名变量b

console.log(a,b)被执行时,会在当前作用域中找到a,b,因此无法访问到外部的同名变量b,输出就是'1,3',而不是正常情况下的'1,2'

ES5中引入了"严格模式"。同正常模式,严格模式在行为上有很多的不同。

在严格模式下执行eval(..)会怎样呢?

function foo(str,a){
       "use strict"
       eval(str)
       console.log(a)//ReferenceError : a is not defined
  }
foo('var a=2')

我们发现在严格模式下foo中使用eval(..)定义的a变量并没用被console.log(a)访问到,这是因为在严格模式下,eval(..)在运行时有了自己的词法作用域,因此其中的变量声明无法修改当前的词法作用域。

除eval(..)之外:

setTimeout(...) setInterval(...) 的第一个参数可以是字符串,字符串的内容会被解释为一段动态生成的函数代码,不提倡使用。

构造函数new Function()的最后一个参数可以接受代码字符串(前面的参数是新生成的函数的形参), 尽量避免使用。

with

with通常被当作重复引用同一个对象中的多个属性的快捷方式,可以不需要重复引用对象本身。他也有一个副作用,会将变量泄漏到全局作用域

  • 我们先来看看with的快捷用法
var obj  ={
    a:1,
    b:2,
    c:3
}
obj.a=2
obj.b=3
obj.c=4
consloe.log(a,b,c)// 2,3,4

with(obj){
    a=3,
    b=4,
    c=5
}

consloe.log(a,b,c)//3,4,5

我能能够看到with可以很方便的访问对象的属性实现快捷赋值。

  • 但是以下这这种情况就会产生副作用了

function foo(obj){
    with(obj){
        a=2
    }
}
var o1={
    a:3
}
var o2={
    b:3
}
foo(o1.a)
console.log(o1.a)//2
foo(o2.a)
console.log(o2.a)//undefined
console.log(a)//2

由输出结果我们看到,o1中有a属性,with将该对象中的a属性的值改为了2,

反观o2with访问后,o2中并没有属性a,那with是不是会在o2中添加一个a属性呢? 由consloe.log(o2.a)输出的结果我们可知,答案是否定的。当我在全局作用域中执行console.log(a)这个操作时,我们惊奇的发现全局作用域中原来始没有定义a变量的,执行这条语句不应该会报错吗?这似乎颠覆我们对JavaScript这们语言的认知。

实际上这就是with副作用,可以理解为,o2中没a属性,而with这"小子"就比较犟,就会在全局作用域中创建了一个全局变量a。

PS:严格模式下with会被禁用。

总结

  • 词法作用域是由函数及变量声明的位置来决定的,在执行过程中也会以此为作用域基准进行变量查找(LHSRHS感兴趣的书中有详细介绍)。

  • 非严格模式下eval(..)with会“欺骗”词法作用域。

  • 词法欺骗的副作用是导致js引擎性能优化失效,使程序运行变慢,因此不建议使用。