块级作用域

块级作用域绑定

传统上,Javascript变量声明机制一直令我们感到困惑。在大多数类C语言在声明变量的同时也会创建变量(绑定)是在声明发生的地方创建的。然而,在JavaScript中,情况并非如此。变量的实际创建位置取决于声明它们的方式,ECMAScript6提供了使控制范围更容易的方法。

var 声明及变量提升(Hoisting)机制

在函数作用域或全局作用域中通过关键字 var 声明的变量,无论实际上是在哪里声明的,都会被当成当前作用域顶部声明的变量,这就是我们常说的提升(Hoisting)机制。下面以一个函数为例来说明:

function getValue(condition) {
  if (condition) {
    var value = "blue";
    // 其他代码
    return value;
  } else {
    // 此处可访问变量value,其值为undefined
    return null;
  }
  // 此处可访问变量value,其值为undefined
}

如果您不熟悉JavaScript,可能会认为只有当 condition 的值为true时才会创建变量 value 。事实上,不管怎样都会创建变量 value 。在编译阶段,JavaScript引擎将上面的 getValue 函数更改为如下所示:

function getValue(condition) {
  var value;
  if (condition) {
    value = "blue";
    // 其他代码
    return value;
  } else {
    return null;
  }
}

变量 value 的声明被提升至函数顶部,而初始化操作依旧留在原处执行,这就意味着在 else 子句中也可以访问到该变量,且由于此时变量尚未初始化,所以其值为 undefined

刚接触JavaScript的开发者通常会花一些时间习惯变量提升,有时还会因误解而导致程序忠出现bug。为此, ECMAScript6引入块级作用域来强化对变量生命周期的控制。

块级声明

块级声明用于声明在指定块的作用域之外无法访问的变量。块级作用域(亦被称为词法作用域)存在于:

  • 函数内部
  • 块中(字符 { 和 } 之间的区域)

很多类C语言都有块级作用域,而ECMAScript6引入块级作用域就是为了让JavaScriptg更灵活也更普适。

let 声明

let 声明语法与 var 的语法相同。基本上可以将 var 替换为 let 来声明一个变量,但将变量的作用域限制为只有当前的代码块(后面会讨论了其他一些细微的差异)。由于 let 声明不会被提升,因此通常会将 let 声明放在封闭代码的顶部,以便整个代码块都可以访问。这里有一个例子:

function getValue(condition) {
  if (condition) {
    let value = "blue";
    // 其他代码
    return value;
  } else {
    // 变量value此处不存在
    return null;
  }
  // 变量value此处不存在
}

现在这个 getValue 函数的运行结果更像类C语言。变量 value 改由关键字 let 进行声明后,不再被提升至函数顶部。执行流离开 if 块,value 立即被销毁。如果 condition 的值为false,就永远不会声明并初始化 value

禁止重复声明

作用域中已经存在某个标识符,此时再使用 let 关键字声明它就会报错,举例来说:

var count = 30;
// 抛出语法错误
let count = 40;

如果当前作用域内嵌另一个作用域,是可以在内嵌作用域中使用 let 声明同名变量,示例:

var count = 30;
if (condition) {
  // 不会抛出错误
  let count = 40;
}

此处的let是在if块内声明了新变量count,因此不会抛出错误。内部块中的count会屏蔽全局作用域中的count,后者只有if块中才能访问到。

const 声明

使用 const 声明的是常量,其值一旦被设定后不可更改。因此,每个通过 const 声明的常量都必须初始化,示例如下:

const maxItems = 30;
// 语法报错,常量未初始化
const name;

const 与 let

const 与 let 声明的都是块级标识符,所以常量也只在当前代码块内有效,一旦执行到块外就会立即销毁。常量同样也不会被提升至作用域顶部,示例如下:

if (condition) {
  const maxItems = 5;
}
// 此处无法访问maxItems

无论是使用var,还是let声明的标识符,在同一个作用域用const声明同名标识符也会抛出语法错误。举例来说:

var message = 'Hello!'
let age = 25;
// 这两天语句都会抛出错误
const message = 'Goodbye';
const age = 30;

const 声明与 let 声明有一个很大的不同,不可以为 const 定义的常量赋值,否则会抛出错误,例如:

const maxItems = 5;
// 抛出语法错误
maxItems = 5;

用 const 声明对象

const 声明不允许修改绑定,但运行修改值。也就是说使用 const 声明对象后,可以修改该对象的属性值。举个例子:

const person = {
  name: 'Nicholas'
}
person.name = 'Greg';
// 抛出语法错误
person = {
  name: 'Greg'
}

临时死区

与 var 不同,let 和 const 声明的变量不会被提升至作用域顶部,如果在声明之前访问这些变量,即使是使用相对安全的 typeof 操作符也会触发引用报错,举个例子:

if (condition) {
  console.log(typeof value); // 引用报错
  let value = 'blue';
}

由于 console.log(typeof value) 语句报错,因此后面的 let 声明变量 value 不会执行。此时的 value 还位于Javascript所谓的临时死区。

但在 let 声明的作用域外对改变量使用 typeof 则不会报错,举个例子:

console.log(typeof value); // undefined
if (condition) {
  let value = 'blue';
}

循环中的块作用域绑定

经常在 for 循环中使用 var i 声明变量

for (var i = 0; i < 10; i++) {
  process(items[i])
}
console.log(i); // 10

使用 let 声明变量就会有不同的结果:

for (let i = 0; i < 10; i++) {
  process(items[i])
}
console.log(i); // 此处i不可访问,抛出一个错误

循环中的函数

var 声明在循环中创建函数比较困难,因为变量到了循环外就不能访问了。例如:

var funcs = [];

for (var i = 0; i < 10; i++) {
  funcs.push(function( ) {
    console.log(i);
  });
}

funcs.forEach(function(func) {
  func(); // 输出10次数字10
});

为了解决这个问题,一般都是在循环中使用立即调用函数表达式(IIFE),就像这样:

var funcs = [];

for (var i = 0; i < 10; i++) {
  funcs.push((function(value) {
    return function( ) {
      console.log(value);
    }
  }(i)));
}

funcs.forEach(function(func) {
  func(); // 输出0,然后1、2,直到9
});

在循环内部,IIFE 表达式为接受每一个变量 i 创建了一个副本并存储了变量 value。这个变量的值就是相应迭代创建的函数所使用的值。

循环中的 let 声明

let 声明模仿上述示例中的 IIFE 所作的一切来简化循环过程,每次迭代循环都会创建一个新变量,并以之前迭代中同名变量的值将其初始化。举个例子:

var funcs = [];

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

funcs.forEach(function(func) {
  func(); // 输出0,然后1、2,直到9
});

对于 for-in 循环和 for-of 循环来说也是一样的,示例如下:

var funcs = [],
    object = {
      a: true,
      b: true,
      c: true
    };

for (let key in object) {
  funcs.push(function( ) {
    console.log(key);
  });
}

funcs.forEach(function(func) {
  func(); // 输出a、b和c
});

循环中的 const 声明

对于普通的 for 循环来说,在初始化变量时使用 const,但是 i++ 更改这个变量的值就会抛出错误,就像这样:

var funcs = [];

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

如果后续循环不会修改该变量,是可以使用 const 声明的。如下示例:

var funs = [],
    object = {
      a: true,
      b: true,
      c: true
    };

// 不会产生错误
for (const key in object) {
  funcs.push(function( ) {
    console.log(key);
  });
}

funcs.forEach(function(func) {
  func(); // 输出a、b和c
});

全局作用域绑定

当 var 被用于全局作用域时,它会创建一个新的全局变量作为全局对象(浏览器环境中的window对象)的属性。这意味着用 var 很可能无意中覆盖一个已经存在的全局属性,就像这样:

// 在浏览器中
var RegExp = 'Hello!';
console.log(window.RegExp); // "Hello!"

var ncz = 'Hi!';
console.log(window.ncz); // "Hi!"

在全局作用域中使用 let 或 const ,会在全局作用域下创建一个新的绑定,但该绑定不会添加为全局对象的属性。就是说,用 let 和 const 不会覆盖全局变量,而只能遮蔽它。示例如下:

// 在浏览器中
let RegExp = 'Hello!';
console.log(Rx); // "Hello!"
console.log((window.RegExp === RegExp); // false

const ncz = 'Hi!';
console.log(ncz); // "Hi!"
console.log((window.ncz === ncz); // false

如果希望在全集对象下定义变量,仍然可以用 var。这种情况常见于在浏览器中跨 frame 或跨 window 访问代码。

posted @ 2020-03-04 16:33  太阳锅锅  阅读(886)  评论(0编辑  收藏  举报