块作用域

任何一对花括号中的语句集都属于一个块,在这之中定义的所有变量在代码块外都是不可见的,我们称之为 块级作用域

尽管函数作用域是最常见的作用域单元,也是现行大多数 JavaScript 最普遍的设计方法,但其他类型的作用域单元也是存在的,并且通过使用其他类型的作用域单元甚至可以实现维护起来更加优秀、简洁的代码,比如块作用域。

声明关键字

var

ES5 及之前是没有块级变量这个说法的,常规性是用 闭包 来防止内存泄漏。

如下所示为 ES5 中 var 声明的一些特点:

  • 函数内的变量若是带 var 声明,则会覆盖外部的全局变量 优先使用
  • 若是函数内部声明变量不带 var 声明,则直接 覆盖同名的全局变量
  • 函数内存在 声明提升 的情况,可以先使用后声明
  • for 循环中的 var 会污染全局(不局限于循环内)

🌰 代码示例:优先使用

var foo = 5;
function bar() {
var foo = 3;
console.log(foo);
}
bar();
// 3

🌰 代码示例:变量提升

var foo = 5;
function bar() {
console.log(foo);
var foo = 3;
}
// JavaScript 允许不存在的变量先使用
// 默认会初始化为一个 undefined
bar();
// undefined,

🌰 代码示例:污染全局

for (var i = 0; i < 9; i++) {
console.log('循环内部' + i);
}
console.log(i);
// 9
console.log(i * 5);
// 45

let

let 声明使用方法基本和 var 相同,而且声明的变量只在其块和子块中可用。 二者之间最主要的区别在于 var 声明的变量的作用域是整个封闭函数。

function foo() {
if(true) {
var number = 5;
console.log(number);
}
console.log(number);
}
function bar({
if(true) {
let number = 5;
console.log(number);
}
console.log(number);
}
foo(); // 5 和 5
bar(); // 5 和 ReferenceError: number is not defined

let 声明的变量的作用域只有外层块,而不是整个外层函数。

我们可以利用这个特性来替代立即执行函数(IIFE)。

/**
* IIFE
*/
(function () {
var number = 1;
// do something
})();
/**
* Block 块级
*/
{
let number = 1;
// do something
}

⚠️ 注意事项

  • 不允许重新声明同名变量,会抛出异常,具有唯一性
  • 不允许没声明就使用,会抛出异常,只有执行该声明的时候才能使用
  • 有自己特色的闭包特性,比如在 for 循环的应用中

const

const 的用法跟 let 差不多,但是 const 一定要赋值,不赋值会报错。

// 用法
const number = 4;
// 没有初始化报错
const t;
// SyntaxError: Missing initializer in const declaration

const 是块级作用域,constlet 的语义相似,就是用来声明常量的,一旦声明了就不能更改。

⚠️ 注意:值得注意的是 const 声明的变量记录的是 指针,不可更改的是 指针,如果 const 所声明的是对象,对象的内容还是可以修改的。

// 重新赋值声明导致报错
const PI = 3.14;
PI = 3.1415926;
// TypeError: Assignment to constant variable.
// 给对象增加属性不会导致 foo 的指针变化,所以不会报错
const foo = { foo: 2 };
foo.bar = 3;
console.log(foo);
// {
// foo: 2,
// bar: 3
// }

⚠️ 注意事项

  • let 一样,具有唯一性,不可重复声明
  • 可以将 const 声明的基本类型变量理解为只读变量,但是其声明的引用类型变量则是可修改的

暂时性死区

使用 letconst 声明的变量,在声明赋值没有到达之前,访问该变量都会导致报错,就连一直以为安全的 typeof 也不再安全。

🌰 代码示例

// TDZ1
function foo() {
// TDZ 开始
console.log(typeof number);
let number = 5; // TDZ 结束
}
foo();
// ReferenceError: number is not defined

报的错是 ReferenceError,如果使用 var 声明的话,number 输出应该是 undefined,从 let 声明的变量的块的第一行,到声明变量之间的这个区域被称作 暂时性死区(TDZ)。凡是在这个区域使用这些变量都会报错。

🌰 代码示例

// TDZ2
function bar() {
console.log(typeof number);
}
bar();
// undefined

在函数里没有用 let 声明 number 的时候,numberundefined,讲道理在 let 声明前也应该是 5,然而 foo 函数却报了错,证明了就算是在未到达 let 声明的地方,但是在用 let 之前已经起到了作用。这是不是说明其实 let 也有提升(这个提升并不是 var 的那种提升,只是有影响),只是在 TDZ 使用的时候报错了,而不是 undefined

事实上,当 JavaScript 引擎检视下面的代码块有变量声明时,对于 var 声明的变量,会将声明提升到函数或全局作用域的顶部,而对 letconst 的时候会将声明放在暂时性死区内。

⚠️ 注意:任何在暂时性死区内访问变量的企图都会导致 运行时错误(Runtime Error)。只有执行到变量的声明语句时,该变量才会从暂时性死区内被移除并可以安全使用。

显式块级作用域

在嵌套的作用域内使用 let 声明同一变量是被允许的。这个嵌套的作用域,在 ES6 中又称 显式块级作用域

var foo = 1;
{
// 不会报错
let = 2;
// other code
}

同时因为是 letconst 是块级作用域,声明的变量在当前块使用完之后就会被释放,所以就算使用相同的标识符也不会覆盖外部作用域的变量, 而 var 是会覆盖外部作用域的变量的。

function foo() {
var bar = 1;
{
let bar = 2;
}
console.log(bar);
}
function zoo() {
var bar = 1;
{
var bar = 2;
}
console.log(bar);
}
foo(); // 1
zoo(); // 2

在 ECMAScript 6 的发展阶段,被广泛认可的变量声明方式是:默认情况下应当使用 let 而不是 var

对于多数 JavaScript 开发者来说, let 的行为方式正是 var 本应有的方式,因此直接用 let 替代 var 更符合逻辑。在这种情况下,你应当对 需要受到保护的变量 使用 const

在默认情况下使用 const ,而只在你知道变量值 需要被更改 的情况下才使用 let 。这在代码中能确保基本层次的不可变性,有助于防止某些类型的错误。