ES2015 let与块级作用域

什么是块级作用域

作用域,顾名思义就是指某个成员可访问的范围。在 ECMAScript5 之前只有两种作用域:

  • 全局作用域
  • 函数作用域

ECMAScript2015 之后新增了块级作用域,这时 ECMAScript 存在三种作用域:

  • 全局作用域
  • 函数作用域
  • 块级作用域

,指的就是一对花括号所包裹起来的范围,比如 if 语句或者 for 语句中的花括号都会产生这里所说的的概念。

if (true) {
	console.log('前端课湛')
}

for (var i = 0; i < 10; i++) {
	console.log('前端课湛')
}

在 ECMAScript2015 版本之前是没有块级作用域的,这就导致在块中定义的成员在外部也可以访问。如下代码所示:

if (true) {
  var foo = "前端课湛";
}
console.log(foo);

上述代码执行的结果如下:

前端课湛

而这一点对于复杂代码是非常不利的,也是不安全的。有了块级作用域之后,可以通过 let 关键字在块级作用域中定义变量。该关键字的用法和 var 是一样的,只不过通过 let 声明的变量只能在当前块级作用域中访问到。

可以将上述示例代码中的 var 修改为 let,如下代码所示:

if (true) {
  let foo = "前端课湛";
}
console.log(foo);

上述代码执行的结果如下:

let-block-scoped.js:10
console.log(foo);
            ^

ReferenceError: foo is not defined
    at Object.<anonymous> (/Users/king/前端课湛/04 整理课程/[拉勾教育]大前端高薪训练营/03 源码/1 JavaScript深度剖析/1.1 ECMAScript新特性/01-let-block-scoped.js:10:13)
    at Module._compile (module.js:635:30)
    at Object.Module._extensions..js (module.js:646:10)
    at Module.load (module.js:554:32)
    at tryModuleLoad (module.js:497:12)
    at Function.Module._load (module.js:489:3)
    at Function.Module.runMain (module.js:676:10)
    at startup (bootstrap_node.js:187:16)
    at bootstrap_node.js:608:3

通过结果 ReferenceError: foo is not defined 可以看到在块级作用域内部定义的成员,在外部是无法访问的。

循环语句中的块级作用域

块级作用域的特性非常适用于循环语句中的计数器。如果循环语句出现了嵌套结构的话,传统方式需要为计数器定义不同的名称,否则就会出现问题。如下代码所示:

for (var i = 0; i < 3; i++) {
  for (var i = 0; i < 3; i++) {
    console.log(i);
  }
}

上述代码中是一个嵌套结构的 for 语句,两层嵌套的计数器都为 3,结果应该是打印 9 次。但是因为外层循环的计数器和内层循环的计数器的名称相同,所以导致只打印 3 次。运行结果如下:

0
1
2

导致这样结果的原因在于内层循环和外层循环的计数器的名称相同,就导致内层循环结束之后的 i 值为 3,外层循环的 i 也为 3 不满足循环条件所以就结束了。

如果定义循环的计数器时使用的是 let 不是 var 的话就不存在上述这样的问题,因为 let 定义的计数器只能在当前循环的语句块中有效。可以将上述代码中的 var 修改为 let,如下代码所示:

for (let i = 0; i < 3; i++) {
  for (let i = 0; i < 3; i++) {
    console.log(i);
  }
  console.log("内层结束 i = " + i);
}

上述代码的运行结果如下:

0
1
2
内层结束 i = 0
0
1
2
内层结束 i = 1
0
1
2
内层结束 i = 2

值得注意的是,这里真正解决问题的是内层循环中的 let 定义的计数器,它将内层循环的计数器与外层循环的计数器进行隔离。所以,即便现在把外层循环的计数器修改为 var 定义的话,也是没有问题的。如下代码所示:

for (var i = 0; i < 3; i++) {
  for (let i = 0; i < 3; i++) {
    console.log(i);
  }
  console.log("内层结束 i = " + i);
}

上述代码的运行结果如下:

0
1
2
内层结束 i = 0
0
1
2
内层结束 i = 1
0
1
2
内层结束 i = 2

虽然 let 解决了嵌套循环计数器同名的问题,但是还是要建议一般情况下不要使用同名的计数器,因为这样不利于后期再去理解代码的含义。

循环注册事件的块级作用域

除此之外,还有一个典型的应用场景就是循环注册事件时,在事件的处理函数中访问循环的计数器。这样的情况下,在没有块级作用域之前会出现一些问题。这里模拟一下为元素注册事件的逻辑,如下代码所示:

var elements = [{}, {}, {}];
for (var i = 0; i < elements.length; i++) {
  elements[i].onclick = function () {
    console.log(i);
  };
}
elements[0].onclick();
elements[1].onclick();
elements[2].onclick();

上述代码的运行结果如下:

3
3
3

之所以是这样的结果,主要原因就是这时打印的 i 全部都是全局作用域中的 i,在循环执行之后 i 的值已经被累加到了 3,所以结果都是 3。

比较熟悉这个问题的可能已经想到这也是闭包的一个经典场景,通过建立闭包结构就可以解决这样的问题。如下代码所示:

var elements = [{}, {}, {}];
for (var i = 0; i < elements.length; i++) {
  elements[i].onclick = (function (i) {
    return function () {
      console.log(i);
    };
  })(i);
}
elements[0].onclick();
elements[1].onclick();
elements[2].onclick();

上述代码的运行结果如下:

0
1
2

其实这里的闭包也就是通过函数作用域来解决全局作用域的影响,现在有了块级作用域之后就不必要这么麻烦了。只需要将声明计数器的 var 修改为 let,如下代码所示:

var elements = [{}, {}, {}];
for (let i = 0; i < elements.length; i++) {
  elements[i].onclick = function () {
    console.log(i);
  };
}
elements[0].onclick();
elements[1].onclick();
elements[2].onclick();

上述代码的运行结果如下:

0
1
2

for 循环中的块级作用域

另外呢,块级作用域在 for 循环中海油一个特别之处。在 for 循环的内部实际上是存在两层作用域,如下代码所示:

for (let i = 0; i < 3; i++) {
  let i = "前端课湛";
  console.log(i);
}

这个时候可能会觉得这两个 i 变量会存在冲突,实际上上述代码的运行结果如下:

前端课湛
前端课湛
前端课湛

通过打印的结果可以看到上述代码中的两个 i 变量是互不影响的,也就是说它们不会在同一个作用域当中。这块可以通过 if 语句的方式将上述 for 循环进行拆解,如下代码所示:

let i = 0;

if (i < 3) {
  let i = "前端课湛";
  console.log(i);
}

i++;

if (i < 3) {
  let i = "前端课湛";
  console.log(i);
}

i++;

if (i < 3) {
  let i = "前端课湛";
  console.log(i);
}

i++;

通过上述拆解之后的代码可以看到 let i = "前端课湛" 是在 if 语句的块级作用域中,而 for 语句的计数器 i 是外层的块级作用域,所以说这两个 i 是互不影响的。

let 不会声明提前

除了产生块级作用域的限制以外,letvar 之间还有一个区别就是 let 的声明不会出现提升的情况。使用 var 声明的变量都会导致这个变量提升到代码的最开始的位置,如下代码所示:

console.log(foo);
var foo = "前端课湛";

上述代码的运行结果如下:

undefined

通过结果可以看到并不是未定义的错误,而是 undefined。这就说明了在打印 foo 变量时该变量就已经声明了,只是还没有赋值而已。这种现象叫做变量声明的提升,但实际上这就是一个 Bug。但开玩笑地说一句,官方的 Bug 不叫 Bug,叫特性。为了纠正这样一个错误,ES2015 中的 let 取消了这样的特性。let 要求必须要先声明变量,然后才能使用变量,否则就会报出未定义的错误。如下代码所示:

console.log(foo);
let foo = "前端课湛";

上述代码的运行结果如下:

let-block-scoped.js:106
console.log(foo);
            ^

ReferenceError: foo is not defined
    at Object.<anonymous> (/Users/king/前端课湛/04 整理课程/[拉勾教育]大前端高薪训练营/03 源码/1 JavaScript深度剖析/1.1 ECMAScript新特性/01-let-block-scoped.js:106:13)
    at Module._compile (module.js:635:30)
    at Object.Module._extensions..js (module.js:646:10)
    at Module.load (module.js:554:32)
    at tryModuleLoad (module.js:497:12)
    at Function.Module._load (module.js:489:3)
    at Function.Module.runMain (module.js:676:10)
    at startup (bootstrap_node.js:187:16)
    at bootstrap_node.js:608:3

至于 ES2015 为什么要新增一个 let 关键字,而不是对 var 关键字进行升级和优化,原因就是如果只是升级和优化 var 关键字的话,就会导致很多以前的项目无法正常工作。

常量

ES2015 中还新增了一个 const 关键字,表示恒量或者常量。所谓常量就是在 let 的基础上多了一个只读的特性,只读的意思就是变量一旦声明之后就不允许再被修改。如下代码所示:

const name = '前端课湛'
name = '前端'

上述代码的运行结果如下:

const.js:2
name = '前端'
     ^

TypeError: Assignment to constant variable.
    at Object.<anonymous> (/Users/king/前端课湛/04 整理课程/[拉勾教育]大前端高薪训练营/03 源码/1 JavaScript深度剖析/1.1 ECMAScript新特性/02-const.js:2:6)
    at Module._compile (module.js:635:30)
    at Object.Module._extensions..js (module.js:646:10)
    at Module.load (module.js:554:32)
    at tryModuleLoad (module.js:497:12)
    at Function.Module._load (module.js:489:3)
    at Function.Module.runMain (module.js:676:10)
    at startup (bootstrap_node.js:187:16)
    at bootstrap_node.js:608:3

再有就是 const 声明常量时必须要设置初始值,声明和赋值不能像 var 关键字一样放在两条语句当中。如下代码所示:

const name
name = '前端课湛'

上述代码的运行结果如下:

const.js:5
const name
      ^^^^

SyntaxError: Missing initializer in const declaration
    at createScript (vm.js:80:10)
    at Object.runInThisContext (vm.js:139:10)
    at Module._compile (module.js:599:28)
    at Object.Module._extensions..js (module.js:646:10)
    at Module.load (module.js:554:32)
    at tryModuleLoad (module.js:497:12)
    at Function.Module._load (module.js:489:3)
    at Function.Module.runMain (module.js:676:10)
    at startup (bootstrap_node.js:187:16)
    at bootstrap_node.js:608:3

这里还有一个需要注意的问题就是,const 声明的成员不能被修改,只是说不允许在声明之后重新去指向一个新的内存地址,并不是说不允许修改常量中的属性成员。如下代码所示:

const obj = {}
obj.name = '前端课湛'

如上述代码的情况,实际上并没有修改 obj 所指向的内存地址,只是修改这块内存空间中的数据。反之,如果将 obj 指向一个新对象,结果就会报错。如下代码所示:

const obj = {}
obj.name = '前端课湛'

obj = {}

上述代码的运行结果如下:

const.js:12
obj = {}
    ^

TypeError: Assignment to constant variable.
    at Object.<anonymous> (/Users/king/前端课湛/04 整理课程/[拉勾教育]大前端高薪训练营/03 源码/1 JavaScript深度剖析/1.1 ECMAScript新特性/02-const.js:12:5)
    at Module._compile (module.js:635:30)
    at Object.Module._extensions..js (module.js:646:10)
    at Module.load (module.js:554:32)
    at tryModuleLoad (module.js:497:12)
    at Function.Module._load (module.js:489:3)
    at Function.Module.runMain (module.js:676:10)
    at startup (bootstrap_node.js:187:16)
    at bootstrap_node.js:608:3

因为这种赋值语句会改变 obj 指向的内存地址。除此之外的特性与 let 关键字的用法基本上是相同的。

最佳实践

加上 ES5 提供的 var 关键字,ES2015 之后有 3 种来声明成员:

  • var 关键字声明成员
  • let 关键字声明块级作用域中的成员
  • const 关键字声明常量

而这 3 个关键字的最佳实践应该是:不用 var,主用 const,配合 let。按照这种方式选择的话,代码质量会明显提高。

其原因也很简单,var 关键字所谓的一些特性实际上都是开发中的陋习,比如先去使用变量再去声明变量。默认使用 const 声明主要的目的就是可以明确这些成员是否会被修改,如果需要修改的话那就可以使用 let 声明。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值