今天看啥  ›  专栏  ›  ihap技术黑洞

窥探 JavaScript 中的变量作用域

ihap技术黑洞  · 掘金  ·  · 2019-10-28 09:07

文章预览

阅读 15

窥探 JavaScript 中的变量作用域

讲解 JavaScript 作用域的文章也有很多了,这里我想聊聊一些不一样的东西。会的读者可以复习,不会的同学可以了解。

在 JavaScript 中,我们可以通过 varlet 来声明变量,也可以通过 const 来定义常量。

但是在 JavaScript 中,变量的作用域一直都很复杂。

目录

  • 1、热身:ES3 的作用域
  • 2、ES6 的作用域
  • 3、浏览器的全局作用域
  • 4、模块作用域

1、热身:ES3 的作用域

在 JavaScript ES3 中,我们只能通过 var 来声明变量,变量在声明时会有变量提升(hoisting),即在后面声明的变量可以被提前访问,而值默认为 undefined

在 ES3 中,最外层的作用域称为全局作用域。如果你在全局作用域下声明变量,这些变量都会被添加到一个全局对象 globalThis 上,成为它的一个属性。

这个 globalThis 在不同环境下指代不同的目标,比如在 Node.JS 中,globalThis 就是 global;在浏览器下,globalThis 就是 window,并且也可以通过 self 来访问。

除了全局作用域以外,ES3 还有三种局部作用域:

  1. 函数作用域 在 ES3 中的函数中,使用 var 来声明变量,所有变量都会提升至函数开头,并且只能在当前函数块内部访问。
    var a = '🍐';
    (function() {
      console.log(a, b); // 🍐, undefined
      var b = '🍐';
      console.log(b); // 🍐
    })();
    console.log(a); // 🍐
    try { console.log(b) } catch(e) { console.error(e.message) } // b is not defined
    复制代码
  2. catch 作用域 在 try { ... } catch(e) { ... } finally { ... } 语句中,变量 e 仅在 catch 块中可以访问。
    try {
      try { console.log(err) } catch(e) { console.error(e.message) } // err is not defined
      throw Error('🍐');
    } catch(err) {
      console.log(err); // Error: 🍐
    } finally {
      try { console.log(err) } catch(e) { console.error(e.message) } // err is not defined
    }
    复制代码
  3. with 作用域 虽然 with 已经不推荐使用了,并且在严格模式(use strict)中已经不可以使用了,但是 with 确实会创造一个局部作用域环境。在 with (obj) {} 语句中,JavaScript 会为 obj 上所有的属性都创建一个局部变量,所有这些变量都只可以在 with 块中访问。
    try { console.log(sin) } catch(e) { console.error(e.message) } // sin is not defined
    with(Math) {
      console.log(sin); // function sin() { [native code] }
    }
    try { console.log(sin) } catch(e) { console.error(e.message) } // sin is not defined
    复制代码

2、ES6 的作用域

在 ES6 中引入了两个新的变量/常量定义方法:letconst。由 letconst 声明/定义的变量/常量没有提升,并且仅在当前块中有效,也就是说,他们是块级作用域。

当你尝试在声明变量/常量之前访问它时,它会提示你“不能在初始化之前访问”,而不是“变量未定义”。这种现象被叫做“临时死区”,而不是“变量提升”。

if (true) {
  try { console.log(a); } catch(e) { console.error(e.message) } // Cannot access 'a' before initialization
  try { console.log(b); } catch(e) { console.error(e.message) } // Cannot access 'b' before initialization
  const a = '🍐';
  let b = '🍐';
  console.log(a, b); // 🍐 🍐
}
try { console.log(a); } catch(e) { console.error(e.message) } // a is not defined
try { console.log(b); } catch(e) { console.error(e.message) } // b is not defined
复制代码

这里 constlet 声明/定义的变量/常量仅在 if 块中可以访问。

甚至,你可以直接写一个块:

{
  const a = '🍐';
  let b = '🍐';
}
复制代码

那么,如果混合 varconst,会发生什么?

(function() {
  var a = '🍐';
  {
    var b = '🍐';
    const a = '🍐🍐';
    const b = '🍐'; // Uncaught SyntaxError: Identifier 'b' has already been declared
  }
})();
复制代码

我们可以看到,即便 a 已经声明/定义,在独立的块中也可以使用 const 来覆盖,重新定义。在块中 a 的值是 🍐🍐,但是离开块后,a 的值还是 🍐

但是,如果在这个独立的块中,使用 var 声明了一个变量 b,虽说 b 会提升至 function 层,但是,在语法解释阶段 const b 就会失败,因为在同一个块中已经声明了 b

3、浏览器的全局作用域

在浏览器中,HTML 允许我们使用 <script> 包裹 JavaScript 代码,并且在同一个 HTML 文档中可以放置多个 <script> 标签。

考虑这段代码:

<script>
var a = '🍐';
let b = '🍐';
const c = '🍐';
</script>
<script>
console.log(a);
console.log(b);
console.log(c);
console.log(self.a);
console.log(self.b);
console.log(self.c);
</script>
复制代码

有两个 <script> 标签,第一个里面声明/定义了三个变量/常量,第二个里面花式访问这些变量/常量,会发生什么?答案是:

🍐
🍐
🍐
🍐
undefined
undefined
复制代码

结果前 4 个输出了🍐,而后两个输出了 undefined

在前面说过,如果你在全局作用域声明了变量,它会被自动添加到全局对象上去。

但是这仅仅是针对 ES3 来说的。

首先,abc 都在全局作用域下,第二个 <script> 也是在全局作用域下的,所以是可以直接访问三个变量/常量的。

但是在 ES6 中,letconst 即便是在全局作用域下声明/定义,也不会将其添加到全局对象上去,所以如果在第二个标签中去通过 self 访问是不存在的。

如果访问不存在的变量,会抛出异常;但是仅仅是访问不存在的属性就没关系,因此后两个返回 undefined

4、模块作用域

考虑这段代码:

<script type="module">
  var a = '🍐';
  let b = '🍐';
  const c = '🍐';
</script>
<script type="module">
  console.group('A');
  try { console.log(a) } catch(e) { console.error(e.message) }
  try { console.log(b) } catch(e) { console.error(e.message) }
  try { console.log(c) } catch(e) { console.error(e.message) }
  try { console.log(d) } catch(e) { console.error(e.message) }
  try { console.log(e) } catch(e) { console.error(e.message) }
  try { console.log(f) } catch(e) { console.error(e.message) }
  console.groupEnd();
</script>
<script defer>
  console.group('B');
  try { console.log(a) } catch(e) { console.error(e.message) }
  try { console.log(b) } catch(e) { console.error(e.message) }
  try { console.log(c) } catch(e) { console.error(e.message) }
  try { console.log(d) } catch(e) { console.error(e.message) }
  try { console.log(e) } catch(e) { console.error(e.message) }
  try { console.log(f) } catch(e) { console.error(e.message) }
  console.groupEnd();
</script>
<script>
  var d = '🍐';
  let e = '🍐';
  const f = '🍐';
</script>
复制代码

首先,一个 <script> 标签,拥有 type="module" 属性,里面声明定义了几个变量/常量;然后,跟着一个 <script> 标签,同样拥有 type="module" 属性,里面尝试访问并打印 abcdef 六个变量/常量;然后又是一个 <script> 标签,内容与第二个几乎一样,除了 group 的内容,没有 type="module" 属性,却多了 defer 属性;最后还是一个 <script> 标签,除了没有 type="module" 属性外,内容与第一个完全一样。

运行结果会怎样?答案是:

┏ B
┣ a is not defined
┣ b is not defined
┣ c is not defined
┣ d is not defined
┣ e is not defined
┗ f is not defined
┏ A
┣ a is not defined
┣ b is not defined
┣ c is not defined
┣ 🍐
┣ 🍐
┗ 🍐
复制代码

猜对了吗?

这里涉及到四个知识点:受到 defer 作用的代码块会被延迟到最后执行;type="module"<script> 默认包含 defer 行为,并且里面定义的变量/常量作用域都是仅影响局部的;对于 inline 内联的 <script> 而言,defer 属性会被忽略。

先看第一个代码块,type="module",因此代码被延迟执行。

然后是第二个代码块,同样是 type="module",代码被延迟执行。

再看第三个代码块,由于这是个内联的脚本(内容直接在标签内给出而不是通过 src 属性指定),因此 defer 属性被忽略,这个脚本还是以正常顺序执行。执行这个代码块会先创建一个 console group,输出一个 B,然后开始依次访问所有变量/常量。但是,看看上面两个代码块,都被延迟执行了,因此此时所有变量都未定义。

然后是第四个代码块,一个普通的 <script> 标签,声明定义了 def 三个变量/常量。

再之后,被延迟的代码块开始依次执行,先是第一个代码块,声明定义了 abc 三个变量/常量,但是由于它是一个 module,因此所有变量/常量仅对自身 module 可见,对外部均不可访问。

最后,是被延迟的第二个代码块,执行这个代码块会先创建一个 console group,输出一个 A,然后开始依次访问所有变量/常量。其中 abc 处于其他 module 中,因此无法访问,而 def 均已声明/定义,因此可以正常访问。

注意,这里不是说 letconst 发生了提升,而仅仅是受到 defer 效果而使得执行顺序发生了改变。


记得要点赞、分享、评论三连,更多精彩内容请关注ihap 技术黑洞!
………………………………

原文地址:访问原文地址
快照地址: 访问文章快照
总结与预览地址:访问总结与预览