详细理解JS作用域的底层原理

  本篇文章在于详细解读JavaScript的作用域,从底层原理来解释一些常见的问题,例如变量提升、隐式创建变量等问题,在和大家一起交流进步的同时,也算对自己知识掌握的记录,方便以后复习

  首先,直接捡干的来,JS作用域大致分为三部分:词法作用域、函数作用域/块作用域、闭包。

  在传统的编译语言中,程序的源代码编译由三个步骤组成:词法分析、语法分析、代码生成。而JS属于动态语言,它的编译过程不发生在构建之前,而是在代码执行前(一般只有几微妙,甚至更短),简单说,任何JS代码执行前都要编译,编译完通常马上就要执行。

  例如: var a = 2; 将其分解为以下步骤:

  1.遇到   var a  编译器会询问作用域是否已经存在同名变量于同一个作用域的集合中,若存在,则忽略该声明。若不存在,编译器在当前作用域声明一个新变量a。

  2.接下来编译器会为引擎生成运行时的代码,这些代码用于处理  a = 2的赋值操作。引擎运行时会询问作用域,当前作用域是否存在变量a,若存在,引擎直接使用该变量。否则引擎继续向上查找,直到顶层全局作用域还未找到,则会抛出ReferenceError,如果找到,就会将2赋值给它。   

  在上面的例子中,引擎有两种查询方式:LHS、RHS。

  其中L、R代表“左”、“右”,是相对于赋值操作的左右,当变量出现在赋值操作的左侧时进行LHS查询,出现在右侧时进行RHS查询,也可以这么理解,RHS查询是找到某个变量的值,而LHS是找到变量的容器本身!!!即作用域中开辟的变量存放空间。举个例子:如下代码   console.log(a); 引擎对a的查询就是RHS,这里没有赋值操作,需要查找a的值,并把它传给console.log(..);函数。在逐级向上查找中,直到全局也没找到,则抛出ReferenceError。但LHS若没找到是不会抛出错误的。具体原因继续看。

  举个例子:  a= 2; 这里对a的引用则是LHS引用,我们并不关心但前值是多少,只是想要为 =2 这个赋值操作找到合法目标,可能有童鞋疑问,=2不就是赋给a的嘛?对啊,但是a到底存不存在呢?在当前作用域中,我们是不知道是否创建了a的存储空间的,如果作用域中存在 var a ,那么该a的存储空间存在,LHS能成功,但是没有a的存储空间呢?也就是a并未创建呢?此时,LHS也不会抛出错误,而是隐式的在当前作用域(全局作用域、即最高层作用域,一层一层找上去的)为我们创建变量a的存储空间,然后把 =2 赋值给a。这也就是为啥 var a =2; 创建的是局部变量,而没有 var 申明的变量是全局变量的原因。

  作用域的嵌套

  

  作用域就是一套如何存储和查找变量的规则。在嵌套作用域中,如上图,在foo()中无法找到变量 b,引擎就会在外层嵌套的作用域中继续查找,直到找到该变量,或者到达最顶层(全局作用域)。上图想要执行console.log()函数,就要对b进行RHS查找,得到其值。才foo中无法完成b的RHS,但在外层中却可以完成。即:引擎从当前的执行作用域中开始查找变量,找不到,则逐级向上查找,直到最顶层作用域。

  区分LHS和RHS

  区分两种查询方式很重要,因为上文简单提到RHS找不到会抛出ReferenceError,而LHS则不会,它会隐式创建所需的变量。如下               

  对b的RHS查找失败,因为没有声明(创建)变量b,未声明的变量,在任何作用域中都无法找到!那么,在上图中,只要把 b= 2放在console.log()之前,函数就成功执行了,因为第一步执行 b= 2;赋值操作进行LHS,找不到,则在全局中隐式创建变量b,此时使用 window.b 是可以得到2的。

词法作用域

  词法作用域就是定义此法阶段的作用域,即你写代码时将变量和作用域写在哪里而决定其作用范围。作用域在查找到第一个标识符时即停止查找,多层嵌套的作用域中同名的标识符,内部遮蔽外部(遮蔽效应),全局遮蔽可用window.得到其值,而局部遮蔽的则无论如何都无法被访问到。无论函数在哪里被调用,以何种方式调用,其词法作用域都只由被声明时所处的位置决定,即你写下哪他就在哪发挥作用。

  

 

上图中,全局作用域中只有一个标识符,即foo,函数foo作用域中有三个标识符,即b,bar ,a 。函数bar里面只有一个标识符 c 。其每个标识符处于不同的作用域中,而代码运行时会以他们不同的位置而访问权限不同。这些位置在书写时已经被我们写死了,他们的作用被我们写好了,这就是词法作用域!代码的位置真的被我们“写死了嘛”?接着看

  词法欺骗

  词法作用域由写代码时声明的位置决定,也可以由两种机制来动态改变词法作用域。

  1.  eval()函数。可接受一个字符串为参数,将其中内容视为好像在书写时就在这个地方的代码。可以理解为我在梦中就是高富帅,真实的就连后面的剧情(梦中剧情,哈哈)都是以高富帅为基础开展的,不知道这个比喻贴切不?即就是eval()可以让里面的参数代码段达到书写时就在这个地方的效果。如下:

  

  输出结果为a:5,b:8,而不是a= 2,根据词法作用域中。foo中找不到a,则到上一层作用域中寻找,上一层中找到了 a = 2 ;。可是eval()函数却欺骗了词法作用域,直接将a放在了foo内部,而导致引擎不需要到外层作用域去查找,直接使用 a = 5 ,从而达到此法欺骗。

2.  with语句。with 语句通常用作重复引用一个对象的多个属性的快捷方式。代码如下:

  

  with语句也可以达到欺骗词法的作用,但是副作用也很明显,造成了变量泄露。原因是调用obj2的时候,其没有变量a,进行LHS查询,最后隐式创建全局变量属性a ,导致变量泄露。

  以上两种词法欺骗方式,第一严重影响性能,第二在严格模式下有诸多限制,所以不建议使用。函数作用域和闭包近期在整理,过几天推出

  作者:方红亮  

  博客:https://www.cnblogs.com/fanghl/p/9369414.html

  今天只介绍了作用域和词法作用域,希望对小伙伴理解有所帮助,分享转载的朋友请注明出处,码字不易,谢谢理解!

版权声明:本文为fanghl原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/fanghl/p/9369414.html