恕我直言!你不是真的懂js中的作用域!

javascript学习之路 专栏收录该内容
25 篇文章 2 订阅

如果对于作用域,词法作用域你还不是很清楚,那么你可就要好好读读这篇文章了,它可是理解闭包的关键!

作用域是什么?

理解作用域

为了便于理解,笔者使用对话的方式进行解释
引擎:负责js程序的编译以及执行过程
编译器:负责语法分析以及代码生成等脏活
作用域:负责收集并维护由所有声明标识符组成的一些列查询,并实施一套非常严格的规则,确定当前执行的代码对标识符的访问权限

假设存在var= 1;看似是一个声明,但是引擎老兄并不这么认为,引擎会觉得这里有两个不同的声明,一个由编译器在编译时运行,另一个由引擎在运行时处理。var a =1 会分解为var a;a=1

  • 首先遇到var a;编译器会询问作用域是否存在a,如果存在,编译器就会忽略,如果不存在,它就会要求作用域在当前作用域的集合中声明一个新的变量,命名为a
  • 然后编译器会为引擎生成运行时所需代码,用于处理a=1这个操作。引擎运行的时候就会询问作用域老哥是否存在a,如果存在就会使用,如果不存在,引擎会继续查找。如果找到就会进行赋值操作,如果找不到的话,就会抛出异常

在引擎查找的过程中,涉及到了两种查询方式

LHS和RHS

  • 当变量出现在赋值操作的左侧时进行LHS查询,出现在右侧时进行RHS查询
  • 通俗易懂的说,可以将其理解为 - - - LHS:赋值操作的目标是谁。RHS:谁是赋值操作的源头

来看下面这一段代码

function foo(a){
    console.log(a);//1
}
foo(1);

foo(…)函数的调用需要对foo进行RHS引用,意味着”去找到foo的值,并把它给我“
其中有一个非常容易被忽略的细节:a=1

这个操作发生在当1被传递给foo()函数时,1会被分配给a,这时为了给a(隐式)分配值,需要进行一次LHS查询,并且还有一次对a的RHS引用,将得到的值传给console.log()。其实console.log()本身也会对console对象进行RHS查询,检查得到的值,是否存在一个叫log的方法。

作用域的嵌套

当一个块或函数嵌套在另一个块或函数中时,就发生了作用域的嵌套。因此,在当前作用域中无法找到某个变量时,引擎就会在外层嵌套的作用域中继续查找,直到找到该变量或抵达全局作用域为止

观察下面这段代码

function(a){
    console.log(a+b);
}
var b=2;
foo(1);  //3

其中对b会进行RHS查询无法在函数foo内部完成,但是可以在上级作用域中完成

引擎:foo的作用域老弟,你这里存在b吗,我对它进行RHS查询
作用域:没有没有,别打扰我泡妞
引擎:foo的上级作用域老哥(全局作用域),你瞅见b了吗,我要要对他进行RHS查询
作用域:看见了看见了,在里面睡觉呢,我拿给你!

遍历嵌套作用域:从当前作用域开始查找,逐级向上查找,直到全局作用域,无论是否找到,均会停止查找

词法作用域

简单的说,词法作用域就是定义在词法阶段的作用域。换句话说,词法作用域是由你在写代码时将变量和块作用域写在哪里来决定的,因此当词法分析器处理代码时会保持作用域不变(大部分情况是如此)

function fn(a){
    var b = a * 2;
    function fn1(c){
        console.log(a,b,c);
    }
    fn1(b*3);
}
fn(2);     //2,4, 12
  1. 首先最外层为全局作用域只有一个标识符:fn。
  2. 第二个作用域为fn所创建的作用域,包含:a,fn1,b
  3. 第三个作用域为fn1所创建的作用域,包含:c
    作用域气泡由其对应的作用域块代码写在哪里决定,逐级包含。

作用域查找会在找到第一个匹配的标识符停止,在多层的嵌套作用域中可以定义同名的标识符,叫做“遮蔽作用”(内部的标识符遮蔽了外部的标识符),如果没有遮蔽的话,就会一直向上级查找直到找到会到最外层作用域停止。
**注意:**全局变量会自动成为全局对象,因此可以不直接通过全局对象的词法名称,而是间接的通过对全局对象属性的引用来对其进行访问(window.a ), 通过这种技术可以访问被同名变量所遮蔽的全局变量,但非全局变量被遮蔽则无法访问到。

函数作用域

函数中的作用域

函数作用域含义:属于这个函数的全局变量都可以在整个函数的范围内使用以及复用。(事实上在嵌套的作用域中也可以使用)

function fn(a){
    var b=1;
    function fn1(){
        //...
    }
    var c = 3;
}

在这个代码片段中,fn的作用域气泡包含了标识符a,b,c,fn1,无论标识符声明出现在作用域何处,这个标识符所代表的变量或函数都依附与所处作用域的气泡。由于a,b,c和fn1都属于fn()的作用域气泡,所以无法从外部访问这些变量,但是在fn()内部可以访问,同样在fn1()也可以进行访问

隐藏内部实现

根据最小特权原则,应该最小限度的暴露必要内容,将其他内容都隐藏起来,比如某个模块的设计或者API设计,促成了这种基于作用域的隐藏方法
function fn(a){
    b = a+fn1(a*2);
    console.log(b*2);
}
function fn1(a){
    return a-1;

}
var b;
fn(2);  //10

上述代码片段中,变量b和fn1()应该是fn()内部具体实现的私有内容,给予外部作用域的访问权限,毫无必要,并且会产生危险,如果被无意使用,导致超出适用条件,那么会是很头疼的事情,可以将上述代码修改如下:

function fn(a){
    function fn1(a){
        return a-1;
    }
    var b;
    b=a+fn1(a*2);
    console.log(b*2)
}
fn(2);  //10

这样b和fn1都无法从外部访问了,功能和效果不受影响,但是设计上将内容私有化。

规避冲突

隐藏作用域中的变量和函数带来的另外一个好处就说可以避免同名标识符之间的冲突
function fn(){
    function fn1(a){
         i =3; //修改for循环所属作用域中的i
         console.log(a+i);
    }
    for( var i=0;i<10;i++){
        fn1(i*2);      //无限循环
    }
}

fn1()函数内部的赋值表达式i=3意外的覆盖了for循环中的i,i被固定设置为3,小于10,导致无限循环

块作用域

你可能没有写过带有块作用域风格的代码,但是这段代码你一定很熟悉
for(var i=0;i<10;i++){
    console.log(i);
}

我们在for循环的头部定义i,通常是只想在for循环内部的上下文中使用i,但i会被绑定在外部作用域中

来看看这段代码

var f=true;
if(f){
    var fn = f*2;
    fn = something(fn);
    console,log(fn);
}

fn的变量仅在if声明的上下文中使用,如果将它声明在if块内部确实很好,但是!当使用var声明变量时,它写在哪里都是一样的,他们最终都会属于外部作用域,所以这仅仅是风格更具有可读性的伪块作用域罢了,要确保不意外使用fn,估计只能靠自觉了。。。

在ES6中,块作用域得到了解决!!!

let

let关键字可以将变量绑定到所在的任意作用域中,(通常是{…}内部),换句话说,let为其声明的变量隐式的劫持了所在的块作用域

更相信的可以看看我以前的博文理解var let const 的区别,这里不过多赘述!

我们再来看for循环

for(let i=0;i<10;i++){
    console.log(i);
}
console.log(i);//ReferenceError

for循环头部的let不仅将i绑定到了for循环的块中,事实上它将其重新绑定到了循环的每一个迭代中,确保上一个循环迭代结束时的值重新进行赋值
用代码来解释:

let j;
for(j=0;j<10;j++){
    let i=j;
    console.log(i);
}

try/catch

很少人会注意到经常使用的try/catch中的catch分句会创建一个块作用域,其中声明的变量仅在catch中有效。
try{
    undefined(); //制造一个异常
}
catch(err){
    console.log(err);//正常执行
}
console.log(err);  //ReferenceError:error not found
//err仅存在catch分句内部,当试图从外部引用时,就会抛出错误!!!

总结

  1. 作用域是一套规则,用于确定在何处以及如何查找变量(标识符)。如果查找的目的是对变量进行赋值,那么就会使用LHS查询,如果目的是获取变量的值,那么就会使用RHS查询。

  2. 不成功的LHS引用会导致自动隐式的创建一个全局变量(非严格模式),改变量使用LHS引用的目标作为标识符。不成功的RHS引用会导致抛出ReferenceError异常

  3. 词法作用域意味着作用域是由书写代码时函数声明的位置来决定的,编译的词法分析阶段基本能够知道全部标识符在哪里以及如何声明的,从而能够预测在执行过程中如何对他们进行查找

  4. 函数是js中最常见的作用域单元,本质上,声明一个函数内部的变量或函数会在所处的作用域中隐藏起来,但函数不是唯一的作用域单元。块作用域指的是变量和函数不仅可以属于所处的作用域,也可以属于某个代码块。

  5. ES3开始,try/catch结构在catch分句中具有块作用域。

写给读者的话:

文章到此也就结束,感谢您的阅读,相逢即是有缘,如果对你有所帮助请点个赞哟,您的点赞就是我前进的动力!!!

下一篇:一篇文章让你搞懂闭包

相关推荐
©️2020 CSDN 皮肤主题: Age of Ai 设计师:meimeiellie 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值