JS中的块作用域和遮蔽效应

JS中的代码块是什么?

我们的程序是由一条条语句构成的,语句是按照自上而下的顺序一条条执行的。代码块(block)也被称为复合语句(coumpound statement),其作用是通过“{ }”将多条语句组合在一起。

{
    // 复合语句
    var a = 10;
    console.log(a);
}

那么为什么要将多条语句组合在一起呢?

因为我们可以将多条语句“打包”在一起,放在一个JS只期待单一语句的地方。这句话看上去有些晦涩,但是配合代码理解很容易:

在下面代码中,条件判断语句if (true) 后面,可以只写一条语句,不写{ }也可以正常运行。

if (true) console.log('hello'); // 只写一行语句,程序也可以正常运行

// 输出 hello

但是如果我们想在条件判断后做更多事情,就需要用到{ },在这个JS只期待单一语句的地方放上更多语句:

if (true) {
    console.log('hello');
    console.log('world');
    console.log('bye'); 
}

块级作用域是什么?

块级作用域(block scope)指的是大括号中的所有变量和函数只可以在这个大括号包含的区域中访问到,在括号外是访问不到的。上代码:

{
    var a = 10; // var处于全局作用域
    let b = 20;
    const c = 30; // let和const都处于代码块作用域
    console.log(a); 
    console.log(b); 
    console.log(c); 
}
console.log(a); 
console.log(b); 
console.log(c); 

将上面的代码放到调试工具中,可以看到var和let、const声明的变量属于不同的作用域。当代码在大括号中执行时,程序可以访问到2个作用域(代码块和全局)中的变量,如下图:

JS中的块作用域和遮蔽效应_第1张图片

但是当代码执行到大括号之外,就会报错,因为当代码块中的语句执行完后,代码块作用域就会消失(从栈中弹出),因此只留下一个全局作用域,其中只绑定了一个变量a,无法访问到变量b和c:
JS中的块作用域和遮蔽效应_第2张图片

因此,该程序最终在控制台的输出结果是:
JS中的块作用域和遮蔽效应_第3张图片
由于在块级作用域外打印b出错,程序会在此处停止运行,不过可想而知,打印c也会出现同样的错误。


遮蔽效应是什么?

不同作用域中相同名称的变量就会触发遮蔽效应(shadowing)。

var的遮蔽效应

看代码,猜结果:

var a = 100;
{
    var a = 10;
    let b = 20;
    const c = 30;
    console.log(a);
    console.log(b);
    console.log(c);
}

控制台打印如下:

在大括号中的变量a会使得第1行声明的变量a无效,这就叫遮蔽效应。因为这两处a指向的都是全局作用域中的a。

那么,如果在括号外再打印一下a呢?

var a = 100;
{
    var a = 10;
    let b = 20;
    const c = 30;
    console.log(a);
    console.log(b);
    console.log(c);
}
console.log(a); // 猜结果

让我们来到调试工具中加入断点:

当程序执行完第1行时,这时全局变量a的值为100:
JS中的块作用域和遮蔽效应_第4张图片

当程序执行完第3行时,全局变量a的值被二次声明的var a = 10重新赋值为10,这两行语句中的var a指向的都是同一个全局变量a:
JS中的块作用域和遮蔽效应_第5张图片

因此,控制台打印如下:
JS中的块作用域和遮蔽效应_第6张图片

let的遮蔽效应

看代码,猜结果:

let b = 100;
{
    var a = 10;
    let b = 20;
    const c = 30;
    console.log(a);
    console.log(b);
    console.log(c);
}
console.log(b);

控制台打印如下:
JS中的块作用域和遮蔽效应_第7张图片
在大括号中的变量b同样会使第1行声明的变量b无效,形成变量遮蔽。但是与var不同的是,let拥有块级作用域,因此最后一行的b打印出来的仍是第1行的声明的b。

让我们再次进入调试工具中加上断点:
JS中的块作用域和遮蔽效应_第8张图片

可以清楚的看到,程序中共存在3个作用域,1个是全局作用域,剩下2个是块级作用域(代码块和脚本)。尽管第1行的let声明处于全局,但是let关键词会自己创造一个块级作用域(图中的脚本),处于内存中独立的空间,储存了值为100的变量b。在大括号中的变量b,处于内存中另一个独立的空间,储存了另一个值为20的变量b。

因此,程序第7行的b访问的是代码块中的变量b,第10行访问的是脚本(也可以理解成另一个代码块)中的变量b。

const的遮蔽效应

const的遮蔽效应和let一致,将之前的代码稍作修改:

const c = 100;
{
    var a = 10;
    let b = 20;
    const c = 30;
    console.log(a);
    console.log(b);
    console.log(c);
}
console.log(c);

控制台打印如下:
JS中的块作用域和遮蔽效应_第9张图片

函数中的遮蔽效应

遮蔽效应不仅发生在块级作用域中,也发生在函数中。看代码:

const c = 100;
function x () {
    const c = 30;
    console.log(c);
}
x();
console.log(c);

控制台打印如下:


非法遮蔽是什么?

如果使用var关键字触发遮蔽效应,是完全可行的,如下:

var a = 100;
{
    var a = 20;
    console.log(a);  // 20
}

同理,使用let关键字触发遮蔽效应也是有效的:

let a = 100;
{
     let a = 20;
    console.log(a);  // 20
}

但是如果用var去遮蔽let,在程序运行之前就会报错:

let a = 100;
{
    var a = 20;
    console.log(a);  // 语法错误
}

那么,如果用let去遮蔽var是否也会报错呢?看代码:

var a = 100;
{
    let a = 20;
    console.log(a); 
}

控制台打印结果如下:

程序并不会报错。那么为什么用var去遮蔽let就会报错呢?

因为遮蔽效应的原则是:重复声明的语句不可以超出自己所处的作用域。也就是,当我们使用var在块级作用域中重新声明变量a时,因为var只有全局作用域或者函数作用域,因此块级作用域无法限制var的声明,这时var a就会超出自己所在的作用域,而let关键字又不允许在同一个作用域中重复声明,因此程序报错。

因此,只需要将上面代码中的块级作用域改成函数作用域,程序就不会报错了:

var a = 100;
function x () {
    var a = 20;
    console.log(a); 
}
x(); // 20
console.log(a); // 100

用const触发遮蔽效应和let的情况同理,因此就不赘述了。

const b = 100;
{
    const b = 20;
    console.log(b); // 20
}

块级作用域和词法作用域

词法作用域就是定义在词法阶段的作用域。换句话说,词法作用域是由你在写代码时将变量和块作用域写在哪里来决定的。

词法作用域的变量查找规则是:如果在当前作用域中找不到该变量,就会去上一层作用域中进行查找,以此类推。块级作用域也享有同样的查找规则:

const a = 20;
{
    const a = 100;
    {
        const a = 200;
        console.log(a); // 200
    }
}
const a = 20;
{
    const a = 100;
    {
        const a = 200;
    }
}
console.log(a); // 20
const a = 20;
{
    const a = 100;
    {
        console.log(a); // 100 往上一级作用域进行查找
    }
}

以上就是JavaScript中块级作用域和遮蔽效应的知识。

你可能感兴趣的:(javascript前端作用域)