JavaScript 块作用域

2019-01-21  本文已影响4人  游学者灬墨槿

块作用域

是一个用来对之前的最小授权原则进行扩展的工具,将代码从在函数中隐藏信息扩展为在块中隐藏信息。

【之前的代码】:

for(var loop = 0; loop < 10; loop++) {
    console.log(loop);
}

【说明】:我们在 for 循环的头部直接定义了变量 loop,通常是因为只想在 for 循环内部的上下文中使用 loop,而忽略了 loop 实际上会被绑定在所处的作用域中。如果能够将 loop 声明在 for 循环内部会是一个很有意义的事情,而现在只能依靠自觉性来防止自己没在作用域其他地方意外地使用 loop 变量。也正是基于这个原因,ES6 推出了块作用域的概念。

【块作用域的用处】:变量的声明应该距离使用的地方越近越好,并最大限度地本地化。

with

with 可以创建一个词法作用域,将对象的属性作为声明在当前作用域的变量(函数)。换言之,with 从对象中创建的作用域仅在 with 声明中而非外部作用域中有效。不可不推荐使用 with 来实现块作用域,在这里提出只是为了说明 with 有这个功能。

try/catch

JavaScript 的 ES3 规范中规定 try/catch 的 catch 分句会创建一个块作用域,其中声明的变量仅在 catch 内部有效。

【注意】:其中声明的变量是指 catch(variable),括号内的变量,而不是在 {} 中声明的变量。

【示例】:

try {
    undefined();
} catch(err) {
    var name = "spirit";
    console.log(err);
}
console.log(name); // spirit
console.log(err);

let

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

【示例】:

var foo = true;

if(foo) {
    let bar = foo * 2;
    bar = something(bar);
    console.log(bar);
}

console.log(bar); // ReferenceError

【注意】:用 let 将变量附加在一个已经存在额块作用域上的行为是隐式的。也就是说,在开发和修改代码的过程中,如果没有密切关注哪些块作用域中有绑定的变量,并且习惯性地移动这些块或者将其包含在其他的块中,将会导致代码变得混乱。

【解决方案】:为块作用域显式地创建块可以部分解决这个问题,使变量的附属关系变得更加清晰。

var foo = true;

if(foo) {
    { // 显示的块
        let bar = foo * 2;
        bar = something(bar);
        console.log(bar);
    }
}

console.log(bar); // ReferenceError

【解释】:只要声明是有效的,在声明中的任意位置都可以使用 {} 括号来为 let 创建一个用于绑定的块。

【注意】:使用 let 进行的声明不会在块作用域中进行提升。声明的代码被执行之前,声明并不存在。

{
    console.log(bar); // ReferenceError!
    let bar = 2;
}

1. 垃圾收集

另一个块作用域非常有用的原因和闭包及回收内存垃圾的回收机制相关。

【示例】:

function process(data) {
    // 在这里做点有趣的事情
}

var someReallyBigData = {
    // ...
};

process(someReallyBigData);

var btn = document.getElementById("my_button");

btn.addEventListener("click", function click(evt) {
    console.log("button clicked");
}, false);

【问题】:click 函数的点击回调并不需要 someReallyBigData 变量。理论上这意味着当 process() 执行后,在内存中占用大量空间的数据结构就可以被垃圾回收了。但是,由于 click 函数形成了一个覆盖整个作用域的闭包,JavaScript 引擎极有可能依然保存着这个结构(取决于具体实现)。

【示例】:

function process(data) {
    // 在这里做点有趣的事情
}

// 在这个块中定义的内容完事可以销毁!
{
    let someReallyBigData = {
        // ...
    };
    
    process(someReallyBigData);   
}

var btn = document.getElementById("my_button");

btn.addEventListener("click", function click(evt) {
    console.log("button clicked");
}, false);

【解释】:使用块作用域可以让引擎清楚地知道没有必要继续保存 someReallyBigData 。

【建议】:为变量显式声明块作用域,并对变量进行本地化绑定是非常有用的工具,可以把它添加到你的代码工具箱中了。

2. let 循环

【示例】:

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

console.log(loop); // ReferenceError

// 等价于
{
    let j;
    for(j = 0; j < 10; j++) {
        let i = j; // 每个迭代重新绑定!
        console.log(i);
    }
}

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

【注意】:由于 let 声明附属于一个新的作用域而不是当前的函数作用域(也不属于全局作用域),当代码中存在对于函数作用域中 var 声明的隐式依赖时,就会有很多隐藏的陷阱,如果用 let 来替代 var 则需要在代码重构的过程中付出额外的精力。

const

除了 let 以外,ES6 还引入了 const,同样可以用来创建块作用域变量,但其值是固定的(常量)。之后任何视图修改值的操作都会引起错误。

小结

附录:块作用域的替代方案

从 ES3 发布以来,JavaScript 中就有了块作用域,而 with 和 catch 分句就是块作用域的两个小例子。

【示例】:

{
    let a = 2;
    console.log(a); // 2
}
console.log(a); // ReferenceError

【问】:如何在 ES6 之前也可以实现上述示例的效果。

【答】:使用 catch。

try { throw 2; } catch(a) {
    console.log(a); // 2
}
console.log(a); // ReferenceError

【解释】:利用 catch 分句具有块作用域来实现,除了丑陋且奇怪了一些,还是挺好用的。

性能

上一篇下一篇

猜你喜欢

热点阅读